Karl Cardenas | Portfolio https://crazykarlcodes.dev/ Welcome to my portfolio Hugo 0.149.1 & FixIt v0.3.20 en [email protected] (Karl Cardenas) [email protected] (Karl Cardenas) Sat, 06 Sep 2025 06:07:14 -0700 Using observability to trace agentic AI workflow decisions https://crazykarlcodes.dev/posts/using-observability-to-trace-agentic-ai-workflow-decisions/ Thu, 04 Sep 2025 20:08:30 -0700[email protected] (Karl Cardenas) https://crazykarlcodes.dev/posts/using-observability-to-trace-agentic-ai-workflow-decisions/ Posts <img src="./images/using-observability-to-trace-agentic-ai-workflow-decisions/hero.webp" alt="featured image" referrerpolicy="no-referrer"><blockquote> <p>This blog was originally published on Spectro Cloud&rsquo;s offical blog site. Click <a href="https://www.spectrocloud.com/blog/using-observability-to-trace-agentic-ai-workflow-decisions"target="_blank" rel="external nofollow noopener noreferrer">here<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> for a direct link to the original blog.</p></blockquote> <p>Like many organizations, we here at Spectro Cloud are exploring how Artificial Intelligence (AI) can help our teams be more productive and improve the experience we give our customers.</p> <p>One clear barrier emerges time and again: agentic development can be challenging to debug and analyze due to the ‘black box’ nature of this technology. In fact, as an industry we struggle to audit the decision-making process of agentic workflows.</p> <p>But that doesn’t mean we shrug and give up. We hold ourselves to high expectations and standards here at Spectro Cloud!</p> <p>For us to release an AI capability in our product, we must first have a deep understanding of how the AI is behaving and attempt to answer critical questions such as <em>“Why did it choose that tool? Why did it take that action? What information did it have at that point in time?”</em> and so on. Only through the understanding of behavioral questions can we produce agentic solutions that provide value — and reliability.</p> <p>To help fellow builders and those pursuing agentic workflows, this blog shares how we’re working to improve our understanding of agentic workflows through observability (O11y), using an example AI application to debug an incorrect output.</p> <h2 class="heading-element" id="open-source-ftw"><span>Open-source FTW</span> <a href="#open-source-ftw" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Commercial platforms exist that provide you with built-in capabilities to ease the development of agentic workflows, which may include observability out of the box.</p> <p>However, not all agentic development platforms offer the same amount of information, and you sometimes have to stitch things together yourself to understand what’s really happening. One open-source solution we stumbled upon early in our journey to building an understanding of our agentic workflows was <a href="https://docs.arize.com/phoenix"target="_blank" rel="external nofollow noopener noreferrer">Arize Phoenix<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>.</p> <p>Phoenix is an observability platform focusing on helping builders answer questions related to their AI applications. Getting started is easy: with a single Docker command, you can start and stand up the observability platform.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run --rm -p 6006:6006 --name phoenix arizephoenix/phoenix:latest</span></span></code></pre></td></tr></table> </div> </div><p>Once the container is ready, you can access the Phoenix dashboard and view traces. Below is an image of a freshly initialized Phoenix dashboard hosted on localhost port 6006.</p> <p><img loading="lazy" src='./images/using-observability-to-trace-agentic-ai-workflow-decisions/phoenix-dashboard.webp' alt="Phoenix dashboard initialization showing a clean interface with project overview and navigation menu"></p> <h2 class="heading-element" id="enabling-tracing"><span>Enabling tracing</span> <a href="#enabling-tracing" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Depending on your agentic framework (assuming you are using a framework), Phoenix supports many integrations that let you get started in seconds with a few one-liners. Check out its integrations <a href="https://docs.arize.com/phoenix/integrations"target="_blank" rel="external nofollow noopener noreferrer">page<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> to view all supported integrations.</p> <p>If you are <em>not</em> using a framework, you can use the SDK to focus on tracing calls to the LLM. You can also add custom functions to define the start and stop of tracing spans. We prefer the latter, as we can inject richer context into the span data by managing the span lifecycle ourselves, but that’s a bit more advanced.</p> <p>Using the Smolagents <a href="https://huggingface.co/docs/smolagents/en/index"target="_blank" rel="external nofollow noopener noreferrer">framework<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> as an example, the steps to get started are:</p> <ol> <li>Install the required Python dependencies.</li> </ol> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pip install arize-phoenix-otel <span class="o">&amp;&amp;</span> <span class="se">\ </span></span></span><span class="line"><span class="cl"><span class="se"></span>pip install openinference-instrumentation-smolagents smolagents</span></span></code></pre></td></tr></table> </div> </div><ol start="2"> <li>Point to where Phoenix is listening, such as localhost:6006. The code snippet below would go into your LLM application.</li> </ol> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span> </span></span><span class="line"><span class="cl"><span class="n">os</span><span class="o">.</span><span class="n">environ</span><span class="p">[</span><span class="s2">&#34;PHOENIX_COLLECTOR_ENDPOINT&#34;</span><span class="p">]</span> <span class="o">=</span> <span class="s2">&#34;http://localhost:6006&#34;</span></span></span></code></pre></td></tr></table> </div> </div><ol start="3"> <li>Register the instrumentation in your Smolagent LLM application.</li> </ol> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">phoenix.otel</span> <span class="kn">import</span> <span class="n">register</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">tracer_provider</span> <span class="o">=</span> <span class="n">register</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">project_name</span><span class="o">=</span><span class="s2">&#34;my-llm-app&#34;</span><span class="p">,</span> <span class="c1"># Default is &#39;default&#39;</span> </span></span><span class="line"><span class="cl"> <span class="n">auto_instrument</span><span class="o">=</span><span class="kc">True</span> </span></span><span class="line"><span class="cl"><span class="p">)</span></span></span></code></pre></td></tr></table> </div> </div><p>That wraps up the steps to get started with Smolagents. There are many more customization options available and advanced features you can enable. The main point to take away from the example is that enabling tracing can be done very quickly, especially if the integration use case you are using has first-class support.</p> <h2 class="heading-element" id="making-sense-of-a-trace"><span>Making sense of a trace</span> <a href="#making-sense-of-a-trace" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Curious readers may wonder at this point what a trace looks like. What information can we glean from a trace? To answer this question, we’ll use an example application using smolagents called “palette-package” with access to an example Model Context Protocol (MCP) server called “palette-mcp”. Assume both the LMM application and the MCP server have been configured to use Phoenix. Below is the view from the Phoenix dashboard.</p> <p><img loading="lazy" src='./images/using-observability-to-trace-agentic-ai-workflow-decisions/phoenix-trace-view.webp' alt="Phoenix trace view dashboard displaying trace data for the palette-package application with timeline and span information"></p> <p>To kick things off, in this example application, assume we fired off a prompt asking “Do I have any active Kubernetes clusters?”. The expected order of operations is for the application to process the query, leverage any tool calls, if available, and return a reply to the user. The MCP server in this example has a single tool function named <strong>getActiveClusters</strong>. The expectation is that the LLM agent will use the function to call to determine if there are any active clusters in the project.</p> <p>The application replied with the following answer.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;data&#34;</span><span class="o">:</span> <span class="s2">&#34;You do not have any active Kubernetes clusters in your project.&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></td></tr></table> </div> </div><p>This is unexpected, as the correct answer is two active clusters in the project of interest. Where in the process could things have gone wrong? The list can be quite exhaustive for any application, especially if multiple tool function calls are expected. The first set of questions that should be answered are:</p> <ol> <li>Did the tool return incorrect data?</li> <li>Did the MCP register correctly?</li> <li>Did the agent parse the data incorrectly from a tool call?</li> <li>Was something misconfigured, such as the projectId or API key that is used to interact with the Palette API?</li> </ol> <p>Getting the answer to these questions will most likely explain the incorrect result, or so we hope. To start the investigation, the trace from the main application, palette-package, should be reviewed. A single span is displayed upon clicking on the palette-package card from the dashboard.</p> <p><img loading="lazy" src='./images/using-observability-to-trace-agentic-ai-workflow-decisions/trace-span.webp' alt="Trace span view showing a single span from the palette-package application with execution details"></p> <p>Clicking on the span row reveals the chain of events, including the tool calls. There is a lot going on here, so to help you follow along, use the annotated image below.</p> <p><img loading="lazy" src='./images/using-observability-to-trace-agentic-ai-workflow-decisions/annotated-trace.webp' alt="Annotated trace view with numbered items highlighting input parameters, nested spans, and tool call results"></p> <p>The first annotated item contains the input received, including the prompt injected in every call to support this example application. The first item checked here is to make sure an API key and project ID is included in the request. The tracing data reveals that an API key and project ID were included. So let’s keep looking.</p> <p>The next item is the nested spans. You can think of this as the order of operations. The application reached out to the MCP server and used a function call. After the function call, the LLM application processed the tool call results and returned the reply. That’s what’s displayed in item number three.</p> <p>By clicking on the nested span titled MCPAdapterTool, more information about the tooling call is revealed, such as what went into it, and what was outputted. From the image below, it’s evident that the LLM interpreted the results correctly: the tool function returned an empty list. The function call also received the expected number of parameters, an API key, and a project ID. The span also reveals that the MCP server was registered correctly, along with its available functions.</p> <p><img loading="lazy" src='./images/using-observability-to-trace-agentic-ai-workflow-decisions/mcp-adapter-tool.webp' alt="MCP adapter tool details showing function call inputs, outputs, and trace status with API key and project ID parameters"></p> <p>The other important piece of information is that the trace status is a green OK. This means the HTTP request against the Palette API returned a 200 HTTP status code. The MCP server function call was configured to set the trace status using the HTTP return code. This lil trick allows us to get more information from a single span.</p> <p>Everything uncovered so far leads to us taking a closer look at the input API key and project ID. Upon reviewing the input to the function call, the project ID provided seems a bit off. The ID ends with 2859c6. That does not align with the Palette project of interest. How did the function call get this project ID? Did the LLM make up this project ID? The first view containing the overall input to the application can reveal the answer.</p> <p><img loading="lazy" src='./images/using-observability-to-trace-agentic-ai-workflow-decisions/input-analysis.webp' alt="Input analysis view revealing the incorrect project ID ending with 2859c6 in the system prompt and configuration"></p> <p>Upon a closer look at the trigger that includes the system prompt, user prompt, and additional context such as AP key and project ID, it’s evident that the application behaved correctly, but received incorrect configurations. The project ID the application received ends with 2859c6 and is not correct. How the incorrect configuration got there is a different story that must be chased up the stack, but from an LLM application perspective, the application can be ruled out as the culprit.</p> <h2 class="heading-element" id="wrapping-up-why-observability-matters-in-a-black-box-world"><span>Wrapping up: why observability matters in a ‘black box’ world</span> <a href="#wrapping-up-why-observability-matters-in-a-black-box-world" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Developing agentic applications is challenging due to AI systems&rsquo; lack of idempotent nature. As we’ve seen, open-source tools like Phoenix by Arize can enable observability in an AI application that gives us clues about what happens inside the ‘black box’.</p> <p>Thanks to trace data, we safely ruled out a number of possible causes. The AI application was not at fault when the system produced an incorrect output. While our example here is not as complex as you might find in a real-world deployment, it certainly shows the value of adding observability to AI applications.</p> <p>If you’re developing agentic applications, we strongly suggest you take the time and effort to set up observability. You can get started in minutes and start developing your AI application with a greater level of confidence. That’s what we do here while researching and investigating many different AI use cases.</p> Tidbyt Application Development Fundamentals https://crazykarlcodes.dev/posts/tidbyt/ Tue, 22 Oct 2024 09:49:01 -0700[email protected] (Karl Cardenas) https://crazykarlcodes.dev/posts/tidbyt/ Posts <img src="./images/tidbyt/cover.webp" alt="featured image" referrerpolicy="no-referrer"><p>I recently purchased a <a href="https://tidbyt.com/"target="_blank" rel="external nofollow noopener noreferrer">Tidbyt<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> second-generation smart display. I was immediately intrigued by the idea of creating custom applications that could present fun messages or images to those who either have to walk by my office or those I interact with in a Zoom call. The first idea that came to mind was to create a clock that rotated through different <a href="https://www.spectrocloud.com/"target="_blank" rel="external nofollow noopener noreferrer">Spectro Cloud<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> graphics. Why Spectro Cloud? That&rsquo;s where I work, so having the logo displayed on my Tidbyt would be a fun way to suprise my coworkers during Zoom calls.</p> <p>Below is a preview of the final product. The clock rotates through different Spectro Cloud graphics every 60 seconds and supports a 24-hour format.</p> <figure><img src="./images/tidbyt/demo.gif" alt="A view of a browser window displaying the intro application"> </figure> <p>I want to share the lessons I learned along the way. This post will help you create custom Tidbyt applications and share them with the world. There are so many possibilities with Tidbyt, and the <a href="https://tidbyt.com/pages/apps"target="_blank" rel="external nofollow noopener noreferrer">official application store<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> already has a wide variety of applications to choose from. Think of this article as a supplemental guide to the <a href="https://tidbyt.dev/docs/build/build-for-tidbyt"target="_blank" rel="external nofollow noopener noreferrer">official documentation<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>.</p> <div class="details admonition tip open"> <div class="details-summary admonition-title"><i class="icon fa-fw fa-regular fa-lightbulb" aria-hidden="true"></i>Don&rsquo;t have a Tidbyt? No problem<i class="details-icon fa-solid fa-angle-right fa-fw" aria-hidden="true"></i></div> <div class="details-content"> <div class="admonition-content">You don&rsquo;t need a physical Tidbyt to start building applications. You can use the <a href="https://github.com/tidbyt/pixlet"target="_blank" rel="external nofollow noopener noreferrer">Pixelet<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> CLI to start a local development server and preview your applications in a web browser.</div> </div> </div> <h2 class="heading-element" id="where-to-start"><span>Where to Start?</span> <a href="#where-to-start" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>The first step is to install <a href="https://github.com/tidbyt/pixlet"target="_blank" rel="external nofollow noopener noreferrer">Pixelet<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>, the Tidbyt development tool. The <a href="https://tidbyt.dev/docs/build/installing-pixlet"target="_blank" rel="external nofollow noopener noreferrer">Installing Pixlet<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> guide provides detailed instructions on how to install Pixelet.</p> <p>Once you have Pixelet installed, create a new project by issuing the following command:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir my-starter-app <span class="o">&amp;&amp;</span> <span class="nb">cd</span> my-starter-app <span class="o">&amp;&amp;</span> pixlet create my-starter-app</span></span></code></pre></td></tr></table> </div> </div><p>You will be prompted for an application name, description, and summary. After you provide the information, Pixelet will create a new directory with the project files.</p> <div class="highlight" data-open="true"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Name (what do you want to call your app?): Sup World </span></span><span class="line"><span class="cl">Summary (what&#39;s the short and sweet of what this app does?): Demo app </span></span><span class="line"><span class="cl">Description (what&#39;s the long form of what this app does?): Demo app. </span></span><span class="line"><span class="cl">Author (your name or your Github handle): Karl Cardenas </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">App created at: </span></span><span class="line"><span class="cl"> /Users/karlcardenas/projects/tidbyt/my-starter-app/sup_world.star </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">To start the app, run: </span></span><span class="line"><span class="cl"> pixlet serve sup_world.star </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">For docs, head to: </span></span><span class="line"><span class="cl"> https://tidbyt.dev</span></span></code></pre></td></tr></table> </div> </div><p>Pixelet will generate two new files, <code>sup_world.star</code> and <code>manifest.yaml</code>. The <code>sup_world.star</code> file is where you will write your application code, and the <code>manifest.yaml</code> file is where you will define the application metadata.</p> <p>Below is the content of the <code>sup_world.star</code> and <code>manifest.yaml</code> files.</p> <div class="highlight" title="manifest.yaml"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l">sup-world</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Sup World</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">summary</span><span class="p">:</span><span class="w"> </span><span class="l">Demo app</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">desc</span><span class="p">:</span><span class="w"> </span><span class="l">Demo App.</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">author</span><span class="p">:</span><span class="w"> </span><span class="l">Karl Cardenas</span></span></span></code></pre></td></tr></table> </div> </div><div class="highlight" data-open="true" title="sup_world.star"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span><span class="lnt">24 </span><span class="lnt">25 </span><span class="lnt">26 </span><span class="lnt">27 </span><span class="lnt">28 </span><span class="lnt">29 </span><span class="lnt">30 </span><span class="lnt">31 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34; </span></span></span><span class="line"><span class="cl"><span class="s2">Applet: Sup World </span></span></span><span class="line"><span class="cl"><span class="s2">Summary: Demo app </span></span></span><span class="line"><span class="cl"><span class="s2">Description: Demo App. </span></span></span><span class="line"><span class="cl"><span class="s2">Author: Karl Cardenas </span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="nb">load</span><span class="p">(</span><span class="s2">&#34;render.star&#34;</span><span class="p">,</span> <span class="s2">&#34;render&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"><span class="nb">load</span><span class="p">(</span><span class="s2">&#34;schema.star&#34;</span><span class="p">,</span> <span class="s2">&#34;schema&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">DEFAULT_WHO</span> <span class="o">=</span> <span class="s2">&#34;world&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">config</span><span class="p">):</span> </span></span><span class="line"><span class="cl"> <span class="n">who</span> <span class="o">=</span> <span class="n">config</span><span class="o">.</span><span class="n">str</span><span class="p">(</span><span class="s2">&#34;who&#34;</span><span class="p">,</span> <span class="n">DEFAULT_WHO</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">message</span> <span class="o">=</span> <span class="s2">&#34;Hello, {}!&#34;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span><span class="n">who</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">render</span><span class="o">.</span><span class="n">Root</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">child</span> <span class="o">=</span> <span class="n">render</span><span class="o">.</span><span class="n">Text</span><span class="p">(</span><span class="n">message</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_schema</span><span class="p">():</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">schema</span><span class="o">.</span><span class="n">Schema</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">version</span> <span class="o">=</span> <span class="s2">&#34;1&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">fields</span> <span class="o">=</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="n">schema</span><span class="o">.</span><span class="n">Text</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">id</span> <span class="o">=</span> <span class="s2">&#34;who&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">name</span> <span class="o">=</span> <span class="s2">&#34;Who?&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">desc</span> <span class="o">=</span> <span class="s2">&#34;Who to say hello to.&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="n">icon</span> <span class="o">=</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"> <span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span></span></span></code></pre></td></tr></table> </div> </div><p>The next step is to start the application by running the following command:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">pixlet serve sup_world.star</span></span></code></pre></td></tr></table> </div> </div><p>Pixelet will start a local development server on <code>http://localhost:8080</code>. You can preview the application in a web browser by navigating to <code>http://localhost:8080</code>.</p> <figure><img src="./images/tidbyt/one.webp"> </figure> <p>Press <code>Ctrl + C</code> to stop the development server. If you are on a Mac, you can press <code>Cmd + C</code>. You will start and stop the development server multiple times throughout the development process.</p> <h2 class="heading-element" id="core-concepts"><span>Core Concepts</span> <a href="#core-concepts" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Now that you have an application available and understand how to start the development server let&rsquo;s explore the core concepts of Tidbyt applications.</p> <h3 class="heading-element" id="programming-language"><span>Programming Language</span> <a href="#programming-language" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><p>Tidbyt applications are written in <a href="https://github.com/bazelbuild/starlark"target="_blank" rel="external nofollow noopener noreferrer">Starlark<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>, which was designed to manage and maintain configuration for the Bazel build system. The syntax may look familiar if you have experience with Python.</p> <div class="details admonition info open"> <div class="details-summary admonition-title"><i class="icon fa-fw fa-solid fa-circle-info" aria-hidden="true"></i>Starlark Implementation<i class="details-icon fa-solid fa-angle-right fa-fw" aria-hidden="true"></i></div> <div class="details-content"> <div class="admonition-content"><blockquote> <p>Tidbyt used the Go implementation of <a href="https://github.com/google/starlark-go/blob/master/doc/spec.md"target="_blank" rel="external nofollow noopener noreferrer">Starlark<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>.</p></blockquote></div> </div> </div> <p>The best resource I found for understanding Starlark&rsquo;s capabilities is the <a href="https://github.com/bazelbuild/starlark/blob/master/spec.md"target="_blank" rel="external nofollow noopener noreferrer">Starlark Language Specification<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> document. This document provides a comprehensive overview of the language and its capabilities, such as built-in functions, data types, and control structures. Do keep this document handy as you build your Tidbyt applications. I found myself referencing it multiple times throughout the development process.</p> <h4 class="heading-element" id="libraries"><span>Libraries</span> <a href="#libraries" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h4><p>Tidbyt provides a set of libraries, also called modules, that you can use to build your applications. You can find the list of available modules on the <a href="https://tidbyt.dev/docs/reference/modules"target="_blank" rel="external nofollow noopener noreferrer">Tidbyt Modules<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> page. I would encourage you to review the modules page and to keep it handy as you build your applications. For example, the <code>load(&quot;render.star&quot;, &quot;render&quot;)</code> render module is the most important module you will use to build your applications. The rendering module provides a set of functions to create and render elements on the Tidbyt display.</p> <h3 class="heading-element" id="rendering-elements"><span>Rendering Elements</span> <a href="#rendering-elements" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><p>You will use the <code>render</code> module to create and display elements on the Tidbyt display. All Tidbyt applications must return a <code>Root</code> element that contains the child elements you want to render. The <code>Root</code> element is the root of the element tree and is the parent of all other elements. Only one <code>Root</code> element is allowed per application.</p> <p>When building my application, I struggled with understanding how to create and render elements. I couldn&rsquo;t find the reference page for the <code>render</code> module. I later learned that the <a href="https://tidbyt.dev/docs/reference/widgets"target="_blank" rel="external nofollow noopener noreferrer">Tidbyt Wiget<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> page acts as the reference page for the <code>render</code> module. The page provides information on using common elements such as <code>Text</code>, <code>Box</code>, <code>Row</code>, and <code>Column</code>, and many other elements that will help you paint your application on the Tidbyt display.</p> <p>My best advice to you is to replace the <code>return render.Root</code> function and experiment with different elements and the code snippets provided on the Widget page to observe how the different elements render on the Tidbyt display. The more you experiment, the more you will understand how to make your vision a reality.</p> <div class="highlight" data-open="true"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"> return render.Root( </span></span><span class="line"><span class="cl"> child = render.Text(message), </span></span><span class="line"><span class="cl"> )</span></span></code></pre></td></tr></table> </div> </div><p>For example, the code below creates a <code>Column</code> element that contains three <code>Box</code> elements. The <code>Box</code> element is a simple element that renders a colored box on the Tidbyt display.</p> <div class="highlight" data-open="true"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span><span class="lnt">8 </span><span class="lnt">9 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"> return render.Root( </span></span><span class="line"><span class="cl"> child = render.Column( </span></span><span class="line"><span class="cl"> children=[ </span></span><span class="line"><span class="cl"> render.Box(width=10, height=8, color=&#34;#a00&#34;), </span></span><span class="line"><span class="cl"> render.Box(width=14, height=6, color=&#34;#0a0&#34;), </span></span><span class="line"><span class="cl"> render.Box(width=16, height=4, color=&#34;#00a&#34;), </span></span><span class="line"><span class="cl"> ], </span></span><span class="line"><span class="cl"> ), </span></span><span class="line"><span class="cl">)</span></span></code></pre></td></tr></table> </div> </div><figure><img src="./images/tidbyt/two.webp" alt="Image of a web browser tab with three columns in different colors stacked on the far left side of the screen"> </figure> <p>If you do this for all the widgets, you will better understand how to create and render elements on the Tidbyt display.</p> <div class="details admonition tip open"> <div class="details-summary admonition-title"><i class="icon fa-fw fa-regular fa-lightbulb" aria-hidden="true"></i>Learn from others<i class="details-icon fa-solid fa-angle-right fa-fw" aria-hidden="true"></i></div> <div class="details-content"> <div class="admonition-content">Check out the <a href="https://github.com/tidbyt/community"target="_blank" rel="external nofollow noopener noreferrer">Tidbyt Community<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> repository on GitHub. You can learn a lot from the available applications in the <code>apps</code> directory.</div> </div> </div> <h3 class="heading-element" id="images"><span>Images</span> <a href="#images" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><p>You can include images with your application. When images are pre-loaded into the application, they must be <code>base64</code> encoded. You can convert an image to base64 format using an online tool such as <a href="https://www.base64-image.de/"target="_blank" rel="external nofollow noopener noreferrer">Base64 Image Encoder<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. Once you have the base64 image, you can use the <code>render.Image</code> element to render the image on the Tidbyt display. The <code>render.Image</code> element has a <code>src</code> property that accepts the base64 image data. You need to import the <code>encoding/base64.star</code> module to decode the base64 image data. The <code>encoding/base64.star</code> module provides a set of functions to encode and decode base64 data. You can use the <code>base64.decode</code> function to decode the base64 image data.</p> <div class="details admonition question open"> <div class="details-summary admonition-title"><i class="icon fa-fw fa-regular fa-circle-question" aria-hidden="true"></i>Question<i class="details-icon fa-solid fa-angle-right fa-fw" aria-hidden="true"></i></div> <div class="details-content"> <div class="admonition-content"><p><strong>Can I import a file instead of hardcoding the base64 image data?</strong></p> <p>Unfortunately, you cannot import an image file directly into the Tidbyt application. You must convert the image to base64 format and hardcode the base64 image data in the application code. The alternative is to download the image from a URL and pass it to the <code>render.Image</code> element&rsquo;s <code>src</code> attribute. Check out the <a href="https://github.com/tidbyt/community/blob/main/apps/lovelandwebcams/loveland_webcams.star#L33-L41"target="_blank" rel="external nofollow noopener noreferrer">Loveland Ski Area Webcams<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> application for an example of how to download an image from a URL and pass it to the <code>render.Image</code> element.</p> </div> </div> </div> <p>The code example below is from the <a href="https://tidbyt.dev/docs/build/crypto-tracker"target="_blank" rel="external nofollow noopener noreferrer">Crypto Tracke<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> guide. The majority of the code is omitted for brevity. The code snippet shows how to decode the base64 image data and pass it to the <code>render.Image</code> element. Keep in mind that the image must be either a PNG, GIF, or JPEG image.</p> <div class="highlight" data-open="true"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="nb">load</span><span class="p">(</span><span class="s2">&#34;encoding/base64.star&#34;</span><span class="p">,</span> <span class="s2">&#34;base64&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="n">BTC_ICON</span> <span class="o">=</span> <span class="n">base64</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s2">&#34;&#34;&#34; </span></span></span><span class="line"><span class="cl"><span class="s2">iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAAlklEQVQ4T2NkwAH+H2T/jy7FaP+ </span></span></span><span class="line"><span class="cl"><span class="s2">TEZtyDEG4Zi0TTPXXzoDF0A1DMQRsADbN6MZdO4NiENwQbAbERh1lWLzMmgFGo5iFZBDYEFwuwG </span></span></span><span class="line"><span class="cl"><span class="s2">sISCPUIKyGgDRjAyBXYXMNIz5XgDQga8TpLboYgux8DO/AwoUuLiEqTLBFMcmxQ7V0gssgklIsL </span></span></span><span class="line"><span class="cl"><span class="s2">AYozjsoBoE45OZi5DRBSnkCAMLhlPBiQGHlAAAAAElFTkSuQmCC </span></span></span><span class="line"><span class="cl"><span class="s2">&#34;&#34;&#34;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="c1"># Later in the code. Pass the image data to the render.Image element.</span> </span></span><span class="line"><span class="cl"> <span class="k">return</span> <span class="n">render</span><span class="o">.</span><span class="n">Root</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">child</span> <span class="o">=</span> <span class="n">render</span><span class="o">.</span><span class="n">Row</span><span class="p">(</span> </span></span><span class="line"><span class="cl"> <span class="n">children</span> <span class="o">=</span> <span class="p">[</span> </span></span><span class="line"><span class="cl"> <span class="n">render</span><span class="o">.</span><span class="n">Image</span><span class="p">(</span><span class="n">src</span><span class="o">=</span><span class="n">BTC_ICON</span><span class="p">),</span> </span></span><span class="line"><span class="cl"> <span class="p">],</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="p">)</span></span></span></code></pre></td></tr></table> </div> </div><h4 class="heading-element" id="size"><span>Size</span> <a href="#size" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h4><p>You need to be mindful of the size of the image you render on the Tidbyt display. The Tidbyt display can display 64x32 pixels. If you render an image larger than 64x32 pixels, the image will be cropped. It may take a few tries to get the image size right. I used the free image editing tools from <a href="https://pinetools.com/c-images/"target="_blank" rel="external nofollow noopener noreferrer">Pinetools<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> to re-size images down to the correct size for my application.</p> <h4 class="heading-element" id="pixelate"><span>Pixelate</span> <a href="#pixelate" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h4><p>A common mistake when building a Tidbyt application is assuming you have to pixelate the image before rendering it on the display. The Tidbyt display is already pixelated, so you do not need to pixelate the image before rendering it on the display. If you pixelate the image before rendering it on the display, the results may differ from what you expect. Instead, focus your attention on the size of the image and ensure its quality.</p> <h3 class="heading-element" id="animations"><span>Animations</span> <a href="#animations" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><p>Tidbyt applications support animations. You can animate elements using the <code>render.Animation</code> element. The <code>animate</code> element provides a set of methods that you can use to manipulate the element&rsquo;s behavior. The list of available methods is on the <a href="https://tidbyt.dev/docs/reference/animations"target="_blank" rel="external nofollow noopener noreferrer">Tidbyt Animation<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> page.</p> <h4 class="heading-element" id="fonts"><span>Fonts</span> <a href="#fonts" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h4><p>Tidbyt supports eight different fonts that you can use in your applications. When using the <code>render.Text</code> function, you can set the <code>font</code> property to one of the eight available fonts. Specify the font by its name. For example, to use the <code>5x8</code> font, set the <code>font</code> property to <code>5x8</code>.</p> <div class="highlight" data-open="true"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">render.Text( </span></span><span class="line"><span class="cl"> &#34;I am a text element&#34;, </span></span><span class="line"><span class="cl"> font = &#34;5x8&#34;, </span></span><span class="line"><span class="cl"> color = &#34;#6a5d9d&#34;, </span></span><span class="line"><span class="cl">),</span></span></code></pre></td></tr></table> </div> </div><p>Review the <a href="https://tidbyt.dev/docs/build/fonts-in-pixlet"target="_blank" rel="external nofollow noopener noreferrer">Fonts in Pixlet<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> page for more information on the available fonts.</p> <h3 class="heading-element" id="architecture"><span>Architecture</span> <a href="#architecture" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><p>Tidbyt has a small section in its documentation where <a href="https://tidbyt.dev/docs/build/authoring-apps#architecture"target="_blank" rel="external nofollow noopener noreferrer">Tidby Architecture<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> is explained. This section is sparse, but it conveys important information such as the display size, caching behavior, secrets management, failure handling, and performance profiling. However, as you comb through the documentation, you will find that the information is scattered throughout the site. Below are some key points to keep in mind as you build your applications.</p> <ul> <li> <p>The default application cycle speed is 15 seconds. This means that Tidbyt will display a different application for most end users every 15 seconds. The user can change this value, but it is important to remember that the default value is 15 seconds. In other words, consider the 15-second cycle when designing your applications. If you have an animation that lasts longer than 15 seconds, the animation will be cut off when the application cycle changes, unless the user has changed the cycle speed, or you use the <code>show_full_animation</code> property to override the user&rsquo;s configuration. Check out the <a href="https://tidbyt.dev/docs/publish/advanced#app-cycle-speed"target="_blank" rel="external nofollow noopener noreferrer">App cycle speed<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> page to learn more about the application cycle speed.</p> </li> <li> <p>If you use the random module, keep in mind the random seed is updated every 15 seconds. This means that if you use the random module to generate random numbers, the numbers will change every 15 seconds.</p> </li> <li> <p>If you use caching, remember that the cache is global and unique per application, not per installation. All Tidbyt applications share the same cache. This means that if you cache a value in your application, then all other instances of your application will use the same cached value. This is important to keep in mind when building applications that rely on caching.</p> </li> <li> <p>Tidbyt pushes a WebP image file to the display. This image file is sent to the display from their servers. During local development, this is simulated through Pixelet. The image file is generated by your local server, which you can preview in a web browser. If you are creating applications where behavior changes, say a clock application that rotates images every 1 minute. You will not observe the change while the local server is active. The reason that Tidbyt and Pixlet send the image file once and only once to the display. In the real world, the application image file is sent from Tidbyt servers to the device right before the application cycle is about to complete for the current application. So, if you expect a new image to be displayed every 1 minute, you must start and stop the Pixelet server to observe the change. In other words, to get a new image to display in development, you must trigger the change with a server restart. There is extensive discussion about this behavior in the following <a href="https://github.com/tidbyt/pixlet/issues/89"target="_blank" rel="external nofollow noopener noreferrer">GitHub issue<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> with potential workarounds.</p> </li> <li> <p>The screen size is 64x32 pixels.</p> </li> </ul> <h3 class="heading-element" id="schema-configuration"><span>Schema Configuration</span> <a href="#schema-configuration" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><p>Each Tidbyt application can accept configuration values from the user. The configuration values are set in the Tidbyt application settings and passed to the <code>main</code> function as a <code>config</code> object. The <code>config</code> object will contain any <a href="https://tidbyt.dev/docs/reference/schema"target="_blank" rel="external nofollow noopener noreferrer">schema<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> fields defined in the schema object.</p> <p>Using the example code from earlier, a function named <code>get_schema</code> returns a schema object that defines the configuration fields that the user can set in the application settings. The schema object contains a list of fields that the user can set. Each field has an <code>id,</code> <code>name,</code> <code>desc,</code> and <code>icon</code> property. The <code>id</code> property is the unique identifier for the field. The <code>name</code> property is the field&rsquo;s name displayed in the application settings. The <code>desc</code> property is the description of the field that is displayed in the application settings. The <code>icon</code> property is the icon that is displayed next to the field in the application settings.</p> <div class="highlight" data-open="true"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">def get_schema(): </span></span><span class="line"><span class="cl"> return schema.Schema( </span></span><span class="line"><span class="cl"> version = &#34;1&#34;, </span></span><span class="line"><span class="cl"> fields = [ </span></span><span class="line"><span class="cl"> schema.Text( </span></span><span class="line"><span class="cl"> id = &#34;who&#34;, </span></span><span class="line"><span class="cl"> name = &#34;Who?&#34;, </span></span><span class="line"><span class="cl"> desc = &#34;Who to say hello to.&#34;, </span></span><span class="line"><span class="cl"> icon = &#34;user&#34;, </span></span><span class="line"><span class="cl"> ), </span></span><span class="line"><span class="cl"> ], </span></span><span class="line"><span class="cl"> )</span></span></code></pre></td></tr></table> </div> </div><p>Each schema field can support different types of input. For example, the <code>schema.Text</code> field supports text input, the <code>schema.Toggle</code> field supports a toggle switch, and the <code>schema.Option</code> field supports a dropdown menu. The schema object also supports default values for each field. The default value is the value that is set when the user installs the application. The user can change the value in the application settings.</p> <p>You can also create more advanced workflows through some of the built-in schema fields. For example, you can trigger an OAuth flow to authenticate the user, get the current geolocation, or use dynamic fields to autogenerate values such as an ID.</p> <p>For a real-world example, check out the Spectro Cloud clock application schema. Users can choose between the following options:</p> <ul> <li>Use a 12-hour format or 24-hour format.</li> <li>Enable or disable the clock.</li> </ul> <p>Below is the schema configuration for the Spectro Cloud clock application.</p> <div class="highlight" data-open="true"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">def get_schema(): </span></span><span class="line"><span class="cl"> return schema.Schema( </span></span><span class="line"><span class="cl"> fields = [ </span></span><span class="line"><span class="cl"> schema.Toggle( </span></span><span class="line"><span class="cl"> id = &#34;clock&#34;, </span></span><span class="line"><span class="cl"> name = &#34;Display Clock&#34;, </span></span><span class="line"><span class="cl"> desc = &#34;Display the clock&#34;, </span></span><span class="line"><span class="cl"> icon = &#34;clock&#34;, </span></span><span class="line"><span class="cl"> default = True, </span></span><span class="line"><span class="cl"> ), </span></span><span class="line"><span class="cl"> schema.Toggle( </span></span><span class="line"><span class="cl"> id = &#34;24hour&#34;, </span></span><span class="line"><span class="cl"> name = &#34;24 Hour Time&#34;, </span></span><span class="line"><span class="cl"> icon = &#34;clock&#34;, </span></span><span class="line"><span class="cl"> desc = &#34;Choose whether to display 12-hour time (off) or 24-hour time (on). Requires Display Clock to be enabled.&#34;, </span></span><span class="line"><span class="cl"> default = False, </span></span><span class="line"><span class="cl"> ), </span></span><span class="line"><span class="cl"> ], </span></span><span class="line"><span class="cl"> version = &#34;1&#34;, </span></span><span class="line"><span class="cl"> )</span></span></code></pre></td></tr></table> </div> </div><p>Inside the <code>main</code> function, you can inspect the <code>config</code> object to get the configuration values the user sets.The <code>config</code> object contains the configuration values set by the user and other important metadata, such as the timezone.</p> <p>The <code>config</code> object has a set of methods that you can use to get the configuration values. For example, you can use the <code>config.get</code> function to get a configuration value by specifying its <code>id</code>. The <code>config.get</code> function returns the configuration value as a string. If you know the field type, you can use specific functions such as <code>config.bool</code> to get a boolean value, or <code>config.str</code> to get a string value.</p> <div class="highlight" data-open="true"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">def main(config): </span></span><span class="line"><span class="cl"> who = config.str(&#34;who&#34;, DEFAULT_WHO) </span></span><span class="line"><span class="cl"> message = &#34;Hello, {}!&#34;.format(who) </span></span><span class="line"><span class="cl"> return render.Root( </span></span><span class="line"><span class="cl"> child = render.Text(message), </span></span><span class="line"><span class="cl"> )</span></span></code></pre></td></tr></table> </div> </div><h2 class="heading-element" id="publish-application"><span>Publish Application</span> <a href="#publish-application" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>If you aspire to share your application with the world, you can publish it in the Tidbyt application store. To publish an application, you must fork the Tidbyt <a href="https://github.com/tidbyt/community"target="_blank" rel="external nofollow noopener noreferrer">Community repository<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. Once you have the repository forked and cloned to your local machine, you can create a new git branch and add your application to the <code>apps</code> directory.</p> <p>Tidbyt has detailed instructions on how to publish an application in the <a href="https://tidbyt.dev/docs/publish/publishing-apps"target="_blank" rel="external nofollow noopener noreferrer">Publishing Apps<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> guide. Check out past Pull Requests (PR) from the community repository to get an idea of how to structure your PR and what information to include in the PR description. Feel free to check out the <a href="https://github.com/tidbyt/community/pull/2671"target="_blank" rel="external nofollow noopener noreferrer">PR<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> for the Spectro Cloud clock application.</p> <h2 class="heading-element" id="additional-help"><span>Additional Help</span> <a href="#additional-help" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>If you need additional help, you can reach out to the Tidbyt community on the <a href="https://discord.gg/r45MXG4kZc"target="_blank" rel="external nofollow noopener noreferrer">Tidbyt Discord<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> channel. You can also use the <a href="https://discuss.tidbyt.com/"target="_blank" rel="external nofollow noopener noreferrer">Tidbyt Forum<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> to ask questions and get help from the community.</p> <h2 class="heading-element" id="closing-thoughts"><span>Closing Thoughts</span> <a href="#closing-thoughts" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Building applications for Tidbyt can be a fun and rewarding experience. The possibilities are endless, and the community is constantly developing new and innovative applications. Use the knowledge provided in this article to start building your own applications. Remember to experiment with different elements, review the available modules, and check out the Tidbyt community repository for inspiration.</p> <a href="https://github.com/tidbyt/community/tree/main/apps/spectro_cloud_clock" title="Spectro Cloud Clock application"target="_blank" rel="external nofollow noopener noreferrer" class="card-link"><span class="cl-backdrop" style="--cl-bg-url: url(/images/fixit.min.svg);"></span> <span class="cl-content"> <span class="cl-text"> <span class="cl-title">Spectro Cloud Clock App</span> <span class="cl-meta"> <svg class="cl-icon-link" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path d="M574 665.4c-3.1-3.1-8.2-3.1-11.3 0L446.5 781.6c-53.8 53.8-144.6 59.5-204 0-59.5-59.5-53.8-150.2 0-204l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3l-39.8-39.8c-3.1-3.1-8.2-3.1-11.3 0L191.4 526.5c-84.6 84.6-84.6 221.5 0 306s221.5 84.6 306 0l116.2-116.2c3.1-3.1 3.1-8.2 0-11.3L574 665.4zM832.6 191.4c-84.6-84.6-221.5-84.6-306 0L410.3 307.6c-3.1 3.1-3.1 8.2 0 11.3l39.7 39.7c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c53.8-53.8 144.6-59.5 204 0 59.5 59.5 53.8 150.2 0 204L665.3 562.6c-3.1 3.1-3.1 8.2 0 11.3l39.8 39.8c3.1 3.1 8.2 3.1 11.3 0l116.2-116.2c84.5-84.6 84.5-221.5 0-306.1z" fill="#a9a9b3"></path><path d="M610.1 372.3c-3.1-3.1-8.2-3.1-11.3 0L372.3 598.7c-3.1 3.1-3.1 8.2 0 11.3l39.6 39.6c3.1 3.1 8.2 3.1 11.3 0l226.4-226.4c3.1-3.1 3.1-8.2 0-11.3l-39.5-39.6z" fill="#a9a9b3"></path></svg> <span class="cl-url">https://github.com/tidbyt/community/tree/main/apps/spectro_cloud_clock</span> </span> </span><img src="true" alt="card image" class="cl-shortcut-image"></span></a> When docs and a dinosaur Git along: enabling versioning in Docusaurus https://crazykarlcodes.dev/posts/when-docs-and-a-dinosaur-git-along/ Wed, 10 Apr 2024 20:08:30 -0700[email protected] (Karl Cardenas) https://crazykarlcodes.dev/posts/when-docs-and-a-dinosaur-git-along/ Posts <img src="./images/docusarus-versioning-git/hero.webp" alt="featured image" referrerpolicy="no-referrer"><p>This blog was originally published on Spectro Cloud&rsquo;s offical blog site. Click <a href="https://www.spectrocloud.com/blog/when-docs-and-a-dinosaur-git-along-enabling-versioning-in-docusaurus"target="_blank" rel="external nofollow noopener noreferrer">here<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> for a direct link to the original blog.</p> <h2 class="heading-element" id="rapid-innovation--great-for-the-product-a-challenge-for-docs"><span>Rapid innovation — great for the product, a challenge for docs!</span> <a href="#rapid-innovation--great-for-the-product-a-challenge-for-docs" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Palette is a powerful, versatile product and it changes fast. We issue <a href="https://www.spectrocloud.com/blog/palette-4-2-brings-innovations-in-edge-resiliency"target="_blank" rel="external nofollow noopener noreferrer">major releases<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> multiple times per year and there are always minor changes to things like our supported packs and environments. </p> <p>This is a good thing: it means we’re keeping pace with the needs of customers like you, and with the innovation of the cloud-native ecosystem.</p> <p>But it creates a challenge for our <a href="https://docs.spectrocloud.com/"target="_blank" rel="external nofollow noopener noreferrer">documentation<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> , too. Users depend on documentation to navigate the many features of Palette, and to get up to speed with what’s changed in each new version. At the time of this writing, our docs code base has over 435 markdown pages containing product information, and it continues to grow. </p> <p>We love a challenge, and our docs team is always looking at ways to improve not only the depth and quality of the content on our docs site, but also how easy it is to consume. For example, you may have <a href="https://www.spectrocloud.com/blog/spectromate-an-open-source-slack-integration-with-mendable"target="_blank" rel="external nofollow noopener noreferrer">already read about our Mendable chatbot integration<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> in a previous blog.</p> <h2 class="heading-element" id="setting-out-on-the-content-versioning-mission"><span>Setting out on the content versioning mission</span> <a href="#setting-out-on-the-content-versioning-mission" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>In late July 2023, we started the groundwork for a much more fundamental innovation: enabling content versioning on our documentation site. </p> <p>Before that, we had to manage all of our documentation data together in a single git branch, clarifying what content applied to what product version in various parts of the documentation. </p> <p>This was difficult for us to manage, but more importantly it made it challenging for our customers to identify what information pertained to specific product versions. This matters, because we give Palette customers using dedicated or self-hosted instances the freedom to stick with older versions of the product until they’re ready to upgrade.</p> <p>Our goal was to implement a way for users to select the Palette version they’re using, and have the docs site show only the information relevant to that version. </p> <p>Versioning is an ‘advanced level’ practice — but for us we knew it was the right approach. </p> <p>In this blog, we’re going to show you how we implemented content versioning with Docusaurus and using git as the source of truth for all versioned content. We’ll also share our solution and explain it in detail.</p> <p>If you’re a Palette customer, we hope you’ll find this an interesting behind-the-scenes tour of a feature that’s valuable to you. </p> <p>And if you or your team are looking at content versioning in your own documentation, we hope this article helps you and your team consider using git as the source of truth for all versioned content versus the default Docusaurus behavior of duplicating folders.</p> <p>To enhance the value of this article to the open source community, and facilitate your understanding of the concepts, we have included a <a href="https://github.com/spectrocloud/docusarus-versioning-template"target="_blank" rel="external nofollow noopener noreferrer">public GitHub repository<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> with all the content code. This repository serves as a practical tool for you to explore and try out our solution. Simply head over to GitHub, click on the green ‘Use this template’ button, and embark on the technical adventure we are about to share.</p> <p>PS: Review the README in the repository for additional information, such as the FAQ.</p> <h2 class="heading-element" id="the-foundation-docs-as-code"><span>The foundation: docs as code</span> <a href="#the-foundation-docs-as-code" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>At Spectro Cloud, we follow a <a href="https://www.writethedocs.org/guide/docs-as-code/"target="_blank" rel="external nofollow noopener noreferrer">Docs as Code<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> philosophy. This means we use the same tooling to create and maintain our documentation as you would use to create, test, deploy, and maintain an application. By following engineering best practices, we are able to deliver complex documentation reliably. </p> <p>We already used the best open-source documentation framework, <a href="https://docusaurus.io/docs"target="_blank" rel="external nofollow noopener noreferrer">Docusaurus<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> , with our codebase in a git repository. Our vision for enabling content versioning was to use different git branches for each minor product release. </p> <figure><img src="./images/docusarus-versioning-git/version-vision.webp" width="300" height="100"> </figure> <p>Like any engineering effort, we started our versioning project by defining our requirements, both for our users and for our technical authors:</p> <h3 class="heading-element" id="user-requirements"><span>User requirements</span> <a href="#user-requirements" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><ul> <li>It’s easy to find documentation that applies to a specific version of Palette.</li> <li>I can easily identify and change my version context.</li> <li>If I need to, I can always access information about prior Palette versions (updates don’t erase old information).</li> </ul> <h3 class="heading-element" id="author-requirements"><span>Author requirements</span> <a href="#author-requirements" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><ul> <li>Enabling versioning doesn’t impact the ability of docs content to scale in the future.</li> <li>Technical authors can target specific versions when updating, creating, and removing content, with minimal overhead.</li> <li>The capabilities of the Docusaurus framework should be leveraged versus creating a custom solution.</li> <li>Enabling versioning does not introduce a user-breaking change, such as URLs not working, etc.</li> </ul> <h2 class="heading-element" id="the-challenge-default-docusaurus-behavior"><span>The challenge: default Docusaurus behavior</span> <a href="#the-challenge-default-docusaurus-behavior" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Docusaurus <a href="https://docusaurus.io/docs/versioning#overview"target="_blank" rel="external nofollow noopener noreferrer">supports content versioning<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> out of the box. The official documentation does a great job of explaining how to enable the feature and what changes you need to make. </p> <p>The default behavior is simple and lends itself well to small documentation sites. Basically, whenever you create a new version, Docusaurus creates a copy of your entire content folder and increments the version number. Docusaurus exposes a command that handles this for you. </p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">npm run docusaurus docs:version 1.1.0</span></span></code></pre></td></tr></table> </div> </div><p>It looks something like this: </p> <figure><img src="./images/docusarus-versioning-git/tree.webp" width="500" height="300"> </figure> <p>The challenge with this approach is that in large documentation sites (such as Spectro Cloud’s), it can lead to large code bases that consume a lot of storage space. </p> <p>The large code base impacts interactions with the versioning control system, especially fresh clones. The latter point is challenging for CI/CD pipeline jobs, as code bases are often cloned, and downloading the code base will take longer. </p> <p>The other challenge is that when there are multiple content folders (one for each version), the folder tree structure becomes more complex. It’s more likely that an author accidentally works on the wrong version folder.</p> <p>The other factor to consider when implementing content versioning is how to handle future backport changes. Backporting is when you have to change, either inserting or removing content from a previous version.</p> <figure><img src="./images/docusarus-versioning-git/backport.webp" width="800" height="400"> </figure> <p>Keep in mind that docusarus does not handle backports: each team is responsible for creating and maintaining their own implementation.</p> <p>Overall, the default Docusarus versioning behavior was not ideal for our team as we felt it did not address our requirements.</p> <h2 class="heading-element" id="git-around-the-challenges"><span>Git around the challenges</span> <a href="#git-around-the-challenges" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>We quickly identified <strong>git</strong> as the solution to address both our scaling concerns and the ability to better control what information and content belonged to a specific version of Palette. </p> <p>The main challenge was that Docusaurus does not integrate with git, at least not out of the box. </p> <p>However, Docusaurus’s simple design for creating versioning content allowed us to create custom logic that built on the out-of-the-box behavior. </p> <p>The next issue was orchestrating when and how to invoke the Docusaurus version command. </p> <p>We decided to develop an orchestration mechanism to enable our vision for using git as the source of truth.</p> <p>After evaluating a range of options, we settled on a bash script, which we named <strong>versions.sh</strong>, to act as the orchestrator for this logic and overall versioning solution, and we built out a set of actions that the orchestration solution steps through every time we run the script. We used the following decision matrix to help us determine the ideal solution for our team. Your team might select a different option depending your team composition, environment and so on.</p> <figure><img src="./images/docusarus-versioning-git/matrix.webp" width="800" height="400"> </figure> <p>If you’re following along using our sample GitHub repo, you can find the overall logic for the orchestrator in the example repository under the <strong>scripts/</strong> folder with the name <a href="https://github.com/spectrocloud/docusarus-versioning-template/blob/main/scripts/versions.sh"target="_blank" rel="external nofollow noopener noreferrer"><strong>versions.sh</strong><i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> .</p> <h2 class="heading-element" id="orchestrator-bashes-into-action"><span>Orchestrator bashes into action</span> <a href="#orchestrator-bashes-into-action" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><figure><img src="./images/docusarus-versioning-git/orchestrator.webp" width="800" height="400"> </figure> <ol> <li>The orchestrator&rsquo;s first step is to fetch all version branches from GitHub. The orchestrator needs to ensure it has the latest changes locally to generate up-to-date versioned content. The script contains some filtering logic to ensure that only version branches are worked on. The orchestrator needs to loop through each version branch, so it must know what all the version branches are.</li> <li>The second step, and the beginning of the loop logic, is to switch git context into each branch.</li> <li>Once inside a version branch, the orchestrator issues the `npm run docusaurus` command to generate the version content for the current branch. </li> <li>That versioned content is moved to a staging area…</li> <li>…along with the respective versions.json file auto-generated by the Docusaurus command. A quick note about versioned content: for each <a href="https://docusaurus.io/docs/docs-multi-instance"target="_blank" rel="external nofollow noopener noreferrer">plugin<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> you create a version for, a sidebars and docs folder is generated. The versions.json file informs Docusaurus how many versions of content are expected. </li> <li>Before exiting the loop (step seven), the orchestrator removes all the versioned content generated. Otherwise, git will complain about uncommitted changes and prevent the orchestrator from…</li> <li>…switching to a different git branch.</li> </ol> <p>Once the loop logic iterates through each version branch and the content has been gathered in the staging area, additional logic can be applied, such as merging all generated versions.json files into a single file. The <strong>versions.json</strong> file is required and expected by Docusaurus when versioning is enabled.  </p> <ol start="8"> <li>Next we move all the versioned content from the staging area back to the root folder of the Docusaurus project. Moving the collective versioned content back to the project root folder along with the <strong>versions.json</strong> file allows Docusaurus to actually use the versioned content for a build or preview with a local development server.</li> <li>The last step of the orchestration flow is to modify the <strong>docusarus.config.js</strong> file. This file expects a configuration definition for each version found in the <strong>versions.json</strong> file. Docusaurus will refuse to start or compile if a <strong>versions.json</strong> file is available and no version definitions are provided to the <strong>docusarus.config.js</strong> . </li> </ol> <h2 class="heading-element" id="streamlining-local-editing"><span>Streamlining local editing</span> <a href="#streamlining-local-editing" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>An important note on that final step. The behavior we describe challenges local development environments, as versioned content is expected. But we needed our technical authors to be able to work on the site locally, including when no versioned content was available. </p> <p>Our solution was to configure <strong>docusarus.config.js</strong> with the following default entry and ensure no <strong>version.json</strong> is available locally. The content in the current branch, either the default branch or a version branch, is simply displayed as the latest content. </p> <figure><img src="./images/docusarus-versioning-git/default.webp" width="800" height="400"> </figure> <p>This configuration allows our authors to start the local development server in all branches, including our main branch, without having to generate versioned content. This latter point is important, as working with versioned content can be taxing on the local development environment (especially on large sites like ours) and slow things down.</p> <p>However, the configuration must look like the following image when versioned content is generated, and a <strong>versions.json</strong> must be available at the root of the project.</p> <figure><img src="./images/docusarus-versioning-git/version-default.webp" width="800" height="400"> </figure> <h2 class="heading-element" id="scripting-versus-manual-updates"><span>Scripting versus manual updates</span> <a href="#scripting-versus-manual-updates" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>We wanted to avoid manually updating the <strong>docusarus.config.js</strong> configuration file every time a new version is introduced or an old version is archived. We also wanted the ability to modify the version label and change the version banner behavior if needed. </p> <p>To do this, we created a NodeJs script that traverses and updates the <strong>docusarus.config.js</strong> file. The script finds the entry for a specific plugin ID, and inserts the values for each version branch. </p> <p>You can find the nodeJs script under <a href="https://github.com/spectrocloud/docusaurus-versioning-template/blob/main/scripts/update_docusaurus_config.js"target="_blank" rel="external nofollow noopener noreferrer">scripts/update_docusarus_config.js<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></p> <p>We chose a NodeJs script because it was the more straightforward approach to modifying a JavaScript file, and our CI environment also had NodeJs installed and available, therefore removing the need to install new tooling or dependencies. </p> <p>To manage the displayed version label and banner, a file named <a href="https://github.com/spectrocloud/docusarus-versioning-template/blob/main/versionsOverride.json"target="_blank" rel="external nofollow noopener noreferrer">versionsOverride.json<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> holds the label and banner configuration . The file contains a small JSON configuration that the <strong>update_docusarus_config.js</strong> consumes and takes into account when creating the new <strong>docusarus.config.js</strong>.</p> <figure><img src="./images/docusarus-versioning-git/banner-label.webp" width="300" height="200"> </figure> <h2 class="heading-element" id="archiving-old-versions"><span>Archiving old versions</span> <a href="#archiving-old-versions" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>For a better user experience, and as <a href="https://docusaurus.io/docs/versioning#keep-the-number-of-versions-small"target="_blank" rel="external nofollow noopener noreferrer">recommended by the official Docusaurus documentation<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> , we minimize the number of active documentation versions visible to users, and offload older versions from the main build to our archive domain. </p> <p>To do this, we use the <a href="https://github.com/spectrocloud/docusarus-versioning-template/blob/main/archiveVersions.json"target="_blank" rel="external nofollow noopener noreferrer">archiveVersions.json<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> file. It contains a reference to a specific version branch and the external URL hosting the archived content — we use <a href="https://www.netlify.com"target="_blank" rel="external nofollow noopener noreferrer">Netlify<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> to host all our archived versions under a different domain. </p> <p>We leverage the <a href="https://docs.netlify.com/site-deploys/overview/#branches-and-deploys"target="_blank" rel="external nofollow noopener noreferrer">branch deploy feature<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> to deliver this experience. From a <strong>docusarus.config.js</strong> perspective, we simply instruct Docusaurus to read from <strong>archiveVersions.json</strong> when defining the version dropdown menu for the docs plugin.</p> <figure><img src="./images/docusarus-versioning-git/archieve.webp" width="700" height="300"> </figure> <p>With this approach, we can ensure we only support three active versions at any time. All other previous versions become external URLs pointing to the Netlify archive domain. </p> <h2 class="heading-element" id="abracadabra-watch-the-magic-happen"><span>Abracadabra… watch the magic happen</span> <a href="#abracadabra-watch-the-magic-happen" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Once all the orchestration steps are complete, we can either start a build with the command `npm run build` or preview the versioned content with the command `npm run start`. </p> <p>Below is a short GIF of the end-to-end flow. Take note of how the orchestration logic switches into different git branches and generates the versioned content. The folders <strong>versioned_docs</strong> and <strong>versioned_sidebars</strong> are generated throughout the process for each version branch and moved to the staging area.</p> <figure><img src="https://assets-global.website-files.com/64196dbe03e13c204de1b1c8/6617fc72acfa0b0390dca39e_CleanShot%202024-04-11%20at%2007.51.19.gif" width="800" height="400"> </figure> <h2 class="heading-element" id="handling-backports"><span>Handling backports</span> <a href="#handling-backports" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>The last step of our technical journey covers how we decided to tackle backporting changes. We wanted an experience that was relatively straightforward for our team to understand and use. We envisioned a flow where an author created a Pull Request (PR) and could choose where to backport the changes from the PR.</p> <p>We investigated a few approaches, including developing our own backport solution. Luckily, we stumbled upon the open-source project <a href="https://github.com/sorenlouv/backport"target="_blank" rel="external nofollow noopener noreferrer">The Backport Tool<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> . We use this tool in a GitHub Action, and through it, we can offer our authors the experience of controlling backports through GitHub labels in a PR.</p> <figure><img src="./images/docusarus-versioning-git/pr-label.webp" width="800" height="400"> </figure> <p>By selecting a label corresponding to a specific version branch, the author can trigger a subsequent PR for the particular version branch after merging the PR. The original PR receives a comment containing the status of each backport PR. </p> <figure><img src="./images/docusarus-versioning-git/pr-comments.webp" width="800" height="400"> </figure> <p>In a merge conflict scenario, the generated status comment will display the reasons for the conflict. These scenarios are handled by git cherry-picking the merged commit into each respective version branch and manually resolving the merge conflict.</p> <figure><img src="./images/docusarus-versioning-git/pr-comments-failed.webp" width="800" height="400"> </figure> <p>If you start the local development server in the example repository, you can find more information about this backport workflow by reviewing the tips and tricks page.</p> <h2 class="heading-element" id="over-to-you"><span>Over to you!</span> <a href="#over-to-you" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>So there you have it: a scripted solution to implement git-based versioned documentation complete with archiving and backporting, already deployed and running great here at Spectro Cloud. </p> <p>We’re pretty proud of what we’ve built, but we know we’re standing on the shoulders of giants. At Spectro Cloud we’re <a href="https://www.spectrocloud.com/community"target="_blank" rel="external nofollow noopener noreferrer">big fans of open source<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> , and over the past five years, we’ve been honored to contribute to projects like LocalAI, Kairos, Validator and SpectroMate. We dedicate this article as a token of appreciation for the <a href="https://docusaurus.io/"target="_blank" rel="external nofollow noopener noreferrer">Docusaurus project<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> and its hard-working maintainers. Everything you just read builds on their amazing work.</p> <p>Now it’s your turn. We hope this article and the <a href="https://github.com/spectrocloud/docusarus-versioning-template"target="_blank" rel="external nofollow noopener noreferrer">associated example repository<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> help spark ideas for how you and your team can tackle your own documentation versions. If you have questions, take a look at the <a href="https://github.com/spectrocloud/docusarus-versioning-template"target="_blank" rel="external nofollow noopener noreferrer">example repository<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> and review the README’s FAQ section.</p> <p>Do you have experience with Docusaurus and git and want to chat about it? Do you have any ideas or suggestions? We would love to hear from you. <a href="https://spectrocloudcommunity.slack.com/join/shared_invite/zt-g8gfzrhf-cKavsGD_myOh30K24pImLA#/shared-invite/email"target="_blank" rel="external nofollow noopener noreferrer">Join our Slack community<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> , where we discuss everything from Kubernetes to open-source.</p> <p>And of course, do <a href="https://docs.spectrocloud.com/"target="_blank" rel="external nofollow noopener noreferrer">take a look at how the version functionality works in practice<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> on our docs site – we hope it makes your experience as a Palette user that bit easier!</p> SpectroMate - An Open-Source Slack integration with Mendable https://crazykarlcodes.dev/posts/spectromate/ Thu, 15 Jun 2023 20:08:30 -0700[email protected] (Karl Cardenas) https://crazykarlcodes.dev/posts/spectromate/ Posts <img src="./images/spectromate/blog-cover.webp" alt="featured image" referrerpolicy="no-referrer"><h2 class="heading-element" id="innovating-the-docs-experience"><span>Innovating the docs experience</span> <a href="#innovating-the-docs-experience" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Like most in the cloud-native community, we’re passionate about great documentation. We’ve felt the joy when discovering a clear, thoughtful guide that helps us get up and running with a new project in our homelabs… and we’ve certainly felt the pain that bad technical writing can cause.</p> <p>So you can understand why our docs team here at Spectro Cloud are always working to improve the experience we offer our users (that’s you). Of course, that includes expanding and refining our content itself — but also innovating how we make that content accessible.</p> <blockquote> <p>SpectroMate is an open-source project I created while working at Spectro Cloud. SpectroMate is an API server with extended functionality designed for Slack integration in the form of a bot. You can use SpectroMate to handle slash commands, and message actions. This article was originally published in Spectro Cloud&rsquo;s blog. Click on the <a href="https://www.spectrocloud.com/blog/spectromate-an-open-source-slack-integration-with-mendable"target="_blank" rel="external nofollow noopener noreferrer">Article Link<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> to read the original source.</p></blockquote> <h2 class="heading-element" id="ready-for-an-ai-docs-chatbot"><span>Ready for an AI docs chatbot?</span> <a href="#ready-for-an-ai-docs-chatbot" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Given all the buzz about ChatGPT (not least the inspirational work that our open source team has been doing with <a href="https://www.spectrocloud.com/blog/k8sgpt-localai-unlock-kubernetes-superpowers-for-free"target="_blank" rel="external nofollow noopener noreferrer">LocalAI<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>), of course we’ve been investigating how we can apply AI innovation and LLMs to the docs experience.</p> <p>To that end, we’ve partnered with <a href="https://www.mendable.ai"target="_blank" rel="external nofollow noopener noreferrer">Mendable.ai<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>, who offer state of the art AI Chat Search technology, which we used to create a chatbot for our product documentation. Once we implement the chatbot on our docs site (it’s in testing right now), you’ll be able to ask it a product question, and get an accurate answer pulled from our knowledgebase.</p> <p>Having an intelligent chatbot for product documentation is a great feature, but we knew it wasn’t enough.</p> <p>Some of the heaviest power users of our docs, and the ones helping train and refine the chatbot, are our internal architects and engineers. We noticed that many of the technical questions and discussions they have happen in Slack channels.</p> <p>Instead of navigating out of Slack and visiting our docs site, wouldn’t it be great if our team members could find the information they need, right there in the chat?</p> <p>And this is where SpectroMate comes in.</p> <h2 class="heading-element" id="spectromate-bringing-mendable-and-more-to-slack"><span>SpectroMate: bringing Mendable (and more) to Slack</span> <a href="#spectromate-bringing-mendable-and-more-to-slack" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>SpectroMate is a new application that we’ve built to bring the Mendable chat experience into Slack. Essentially, it’s an API server with extended functionality designed for Slack integration. It comes with out-of-the-box support for <a href="https://www.mendable.ai"target="_blank" rel="external nofollow noopener noreferrer">Mendable.ai<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> chatbots, but can easily be extended.</p> <br /> <figure><img src="./images/spectromate/architecture.webp"> </figure> <p>We’ve been using SpectroMate internally for several weeks, and it’s running great. Users on our internal company Slack can ask a docs question to the Mendable chatbot just by using the <code>/docs ask</code> command in any Slack channel.</p> <br /> <figure><img src="./images/spectromate/usage.webp"> </figure> <p>Already SpectroMate has helped us improve and fine-tune the Mendable model based on internal usage feedback, all from the comfort of Slack. This gives us a higher degree of confidence in our language model before opening it up to the general public.</p> <h2 class="heading-element" id="spectromate-is-open-source"><span>SpectroMate is open source!</span> <a href="#spectromate-is-open-source" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>We ♥️ OSS. So we’re excited to announce we’ve made <a href="https://github.com/spectrocloud/spectromate"target="_blank" rel="external nofollow noopener noreferrer">SpectroMate<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> open source.</p> <p>If you want to kick the tires, you can use Terraform to deploy it to our <a href="https://docs.spectrocloud.com/devx"target="_blank" rel="external nofollow noopener noreferrer">Palette Dev Engine<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> environment in under five minutes by following the <a href="https://github.com/spectrocloud/spectromate/blob/main/docs/getting-started.md"target="_blank" rel="external nofollow noopener noreferrer">Getting Started guide<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. Just be sure to fill in our form to request access to Palette first!</p> <p>SpectroMate is available as a standalone binary, or you can use the Docker image to deploy the application to any computing environment. If you prefer to deploy SpectroMate to your own Kubernetes environment, you can do so by following the <a href="https://github.com/spectrocloud/spectromate/blob/main/deployment/k8s-generic/README.md"target="_blank" rel="external nofollow noopener noreferrer">Kubernetes deployment instructions<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>.</p> <p>The out-of-the-box support for Mendable will be helpful if you want to follow in our footsteps and expose your documentation as an AI chatbot to internal users or community Slack workspaces.</p> <p>You can customize SpectroMate by adding new API endpoints, new slash commands, or new functionality for your Slack workspace. Fork the GitHub project and start playing! Of course, we also welcome contributions. SpectroMate is written in Go and is designed to be simple to maintain and expand.</p> <p>If you want to explore the inner workings of SpectroMate, refer to the <a href="https://github.com/spectrocloud/spectromate/blob/main/docs/internal.md"target="_blank" rel="external nofollow noopener noreferrer">technical internals<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> document.</p> <p>And as always, if you have any questions, concerns, or feedback, join our community <a href="https://join.slack.com/t/spectrocloudcommunity/shared_invite/zt-g8gfzrhf-cKavsGD_myOh30K24pImLA"target="_blank" rel="external nofollow noopener noreferrer">Slack channel<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>.</p> Cloud Cost Questions for Engineering Managers https://crazykarlcodes.dev/posts/cost-advice/ Sat, 06 Nov 2021 12:45:14 -0700[email protected] (Karl Cardenas) https://crazykarlcodes.dev/posts/cost-advice/ Posts <img src="./images/cost-advice/money_spill.webp" alt="featured image" referrerpolicy="no-referrer"><p>You have assumed the leadership of a team that is operating in a cloud environment. It’s a new beginning, you are excited about the future (hopefully), the team members, and most of all, the thrill of a new challenge. After the excitement settles down you start asking questions to better understand the work and the team. Among the list of questions you have, you should include questions pertaining to cloud cost and cost optimization.</p> <blockquote> <p>This article was originally published on Medium. Link to the Medium article can be found <a href="https://cardenas88karl.medium.com/cloud-cost-questions-for-engineering-managers-dae9577a9aef"target="_blank" rel="external nofollow noopener noreferrer">here<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>.</p></blockquote> <p>In this article, you will find a set of questions that are beneficial for you and your team to further explore. These are questions I have found beneficial in the past and I believe they will be beneficial to you too. Without further ado, let’s dive into it.</p> <h2 class="heading-element" id="q-do-we-have-any-budget-alarms-established"><span>Q: Do we have any budget alarms established?</span> <a href="#q-do-we-have-any-budget-alarms-established" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>This is a simple question but the answer will reveal a lot of information about the team, the organization, and the emphasis placed on cost management. The ideal answer is “yes”, and depending on the maturity of the team, and the organization you might find out that there are several layers of budget alarms. Sometimes, these budgets are for different services and environments. If you are in the ”yes” camp, give the team kudos 👏</p> <p>For those of you who find yourself in the “no” camp ⛺️ , don’t despair. Yes, there is a lot of work to do here, but it’s also an opportunity to stand out, and raise the standard. All major cloud providers offer the ability to set budget alarms. There are many ways to use the budget alarms, but the primary reason you want to use alarms is for cost awareness and to change behavior. Yes, behavior change. You want to get you and your team to take a moment and ask themselves “what impact will this change have on the budget”. The alarms help remind the team to act more responsibly from a financial perspective. If there is no set budget, then review the billing information for the past three to six months and identify a baseline/average.</p> <p>Budgets are not a one-and-done kind of deal. Budgets are a moving goal and should be reviewed often. You want to aim for a goal but the reality is that accurately forecasting cost is difficult and often subject to change due to many external factors. As you and your team develop a good understanding of what the major cost drivers are, you can then start having conversations on how to reduce the expenses. But it all starts with measuring. As the saying goes “What gets measured, get’s done”</p> <h2 class="heading-element" id="q-is-there-a-tagging-policy-in-place"><span>Q: Is there a tagging policy in place?</span> <a href="#q-is-there-a-tagging-policy-in-place" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Tagging is important for teams to more accurately understand their cost, but it’s critical at the organizational level to understand where financial resources are being allocated. Let’s break this down further, starting at the team level.</p> <p>By tagging resources, you and your team can more accurately understand expense reports generated by the cloud provider. Let’s use a real-world example. Assume you and your team have a fleet of virtual machines (VMs). A VM can belong to a different part of the application architecture. Without tagging, how would you identify which part of the architecture is costing X amount of dollars in a given month? Take this example a bit further, assume it’s a multi-tenant environment. If various teams are consuming VMs without tagging, it would be very difficult to understand the cost of each team. You could use a naming convention to identify different teams or parts of your application architecture but that will not help you break down the cost of VMs at the end of the month when you are reviewing the bill. Cloud service costs can be broken down by tags. The ability to break down service costs by tags is why tagging is important from a cost management perspective. The two screenshots below illustrate this. The bottom image showcases how the EC2 cost for the month of August is narrowed down to the resources with the tag 12345.</p> <figure><img src="./images/cost-advice/one.webp"><figcaption> <h4>Screenshot of AWS Cost Explorer</h4> </figcaption> </figure> <figure><img src="./images/cost-advice/two.webp"><figcaption> <h4>The cost of 12345 in relationship to EC2 utilization</h4> </figcaption> </figure> <p>Let’s go back to the fleet of VMs example. By understanding the cost for the different components of the architecture, you and your team can now have a meaningful discussion related to the expenses. Perhaps the cost is justified, or maybe there is room for optimization?</p> <p>Let’s go up a layer and look at it from an organizational perspective. Similar to an application that is made up of numerous components, the same applies to an organization and the teams that make up the organization. Organizations develop budget plans in order to remain profitable and responsibly manage their expenses. From a cloud consumption perspective, this includes understanding how the teams are utilizing cloud resources. Below are a few questions commonly asked by organizations:</p> <ul> <li>Development costs vs production costs</li> <li>The total storage cost and growth rate</li> <li>Fixed cloud costs vs team variable cost (primary infrastructure vs application infrastructure)</li> <li>Compute resource utilization</li> </ul> <p>Without tagging, it’s difficult to break down costs for different environments. Another reason why tagging is important to organizations is to hold leaders accountable for their team’s actions. Leaders play a critical role in helping the organization meet budget goals. If leaders are not held accountable for the cost their team is incurring then it’s hardly surprising if the organization fails to stay within the planned budget. Tagging allows the organization better understand the cost of each team and how much to budget for future expenses.</p> <p>If organizations have a good grasp on their cloud expenses and resource utilization, they can start researching the purchase of reserved compute instances or pre-pay for resources and receive a discount. This may sound counterintuitive at first but it can go a long way in helping organization reduce their cost.</p> <p>Tagging is not a perfect solution, but it goes a long way in helping demystify the cost incurred in public cloud environments. There are more benefits to tagging besides financial reporting. Tagging can be used to help drive automation, labeling data, security, and other meta-data purposes. AWS has an example of tagging categories that can be found here.</p> <h2 class="heading-element" id="q-how-are-resources-primarily-provisioned"><span>Q: How are resources primarily provisioned?</span> <a href="#q-how-are-resources-primarily-provisioned" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>How is your team deploying cloud resources? Are they doing “ClickOps”, meaning they utilize a user interface and click their way through deploying the required infrastructure? Or are they writing everything down, and deploying resources through infrastructure as code (IaC)? This could be Terraform, CloudFormation, ARM Templates, or other IaC flavors.</p> <p>The preferred behavior is to leverage infrastructure as code and avoid manually deploying resources. Manually deploying resources increase opportunities for mistakes to happen during the deployment process and the cleanup process. Someone could forget to deploy a required resource, assign the required permission, or perhaps assign a value to a required environment variable. It’s also likely that something will be missed during the cleanup process. It would be a shame if expensive resources such a managed Kubernetes clusters, data warehouse clusters, or MapReduce clusters were left running over the weekend not utilized</p> <p>There are many reasons why teams should <a href="./posts/infrastructure-as-code/">leverage infrastructures as code</a>, but that’s outside of the scope of this article. Many of the popular IaC tools help with cost management. Such as Terraform Cloud’s cost estimation and Sentinel policies. There are also free solutions available, such as <a href="https://github.com/antonbabenko/terraform-cost-estimation"target="_blank" rel="external nofollow noopener noreferrer">terraform-cost-estimation<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>, aws estimate-template-cost, <a href="https://www.infracost.io/"target="_blank" rel="external nofollow noopener noreferrer">infracost<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>, to mention a few.</p> <figure><img src="./images/cost-advice/three.webp"><figcaption> <h4>Terraform Console</h4> </figcaption> </figure> <p>Terraform Cloud Estimation feature example The neat aspect of these IaC tools is that you can use them with an open-source tool called Open Policy. Open Policy is a framework that allows administrators to write rules for what is allowed and disallowed to be deployed. This can be used to enforce standards in teams but it can also be used to prevent mismanagement of cloud resources. This greatly helps you as the leader of the team prevent accidents or misconfiguration from being deployed.</p> <h2 class="heading-element" id="q-is-there-automation-in-place-to-remove-idle-resources"><span>Q: Is there automation in place to remove idle resources?</span> <a href="#q-is-there-automation-in-place-to-remove-idle-resources" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>This is an investment that all organizations and teams should invest in. Accidents happen, priorities change, and the potential for resources not being cleaned up can happen to the best of us. A simple automation script that pauses compute instances after a specific time (cron based) is a simple investment that can help reduce unnecessary expenses. Below is a simple example for pausing EC2 instances I’ve used in the past. The script below is executed by an AWS Lambda and triggered by a CloudWatch rule.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">boto3</span> </span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">lambda_handler</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span> </span></span><span class="line"><span class="cl"> <span class="n">client</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">client</span><span class="p">(</span><span class="s1">&#39;ec2&#39;</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="c1"># ec2_regions = [region[&#39;RegionName&#39;] for region in client.describe_regions()[&#39;Regions&#39;]]</span> </span></span><span class="line"><span class="cl"> <span class="n">ec2_regions</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;us-east-1&#34;</span><span class="p">,</span> <span class="s2">&#34;us-east-2&#34;</span><span class="p">,</span> <span class="s2">&#34;us-west-1&#34;</span><span class="p">,</span> <span class="s2">&#34;us-west-2&#34;</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">region</span> <span class="ow">in</span> <span class="n">ec2_regions</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="n">ec2</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">resource</span><span class="p">(</span><span class="s1">&#39;ec2&#39;</span><span class="p">,</span><span class="n">region_name</span><span class="o">=</span><span class="n">region</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="n">instances</span> <span class="o">=</span> <span class="n">ec2</span><span class="o">.</span><span class="n">instances</span><span class="o">.</span><span class="n">filter</span><span class="p">(</span><span class="n">Filters</span><span class="o">=</span><span class="p">[{</span><span class="s1">&#39;Name&#39;</span><span class="p">:</span> <span class="s1">&#39;instance-state-name&#39;</span><span class="p">,</span> <span class="s1">&#39;Values&#39;</span><span class="p">:</span> <span class="p">[</span><span class="s1">&#39;running&#39;</span><span class="p">]}])</span> </span></span><span class="line"><span class="cl"> <span class="n">RunningInstances</span> <span class="o">=</span> <span class="p">[</span><span class="n">instance</span><span class="o">.</span><span class="n">id</span> <span class="k">for</span> <span class="n">instance</span> <span class="ow">in</span> <span class="n">instances</span><span class="p">]</span> </span></span><span class="line"><span class="cl"> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="n">RunningInstances</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="n">stoppingInstances</span> <span class="o">=</span> <span class="n">ec2</span><span class="o">.</span><span class="n">instances</span><span class="o">.</span><span class="n">stop</span><span class="p">(</span><span class="n">i</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="nb">print</span><span class="p">(</span><span class="n">stoppingInstances</span><span class="p">)</span> </span></span><span class="line"><span class="cl"> <span class="k">else</span><span class="p">:</span> </span></span><span class="line"><span class="cl"> <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;No instances stopped&#34;</span><span class="p">)</span></span></span></code></pre></td></tr></table> </div> </div><p>However, many more powerful tools can help you with this type of automation. The open-source tool Cloud Custodian is an excellent resource that many organizations and teams use to maintain their cloud environments. The key is to be proactive and address the most likely scenario that could lead to unnecessary expenses.</p> <h2 class="heading-element" id="closing"><span>Closing</span> <a href="#closing" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>In this article, we looked at a few cost-related questions that all engineering managers should ask themselves and their teams operating in cloud environments. The questions discussed in this article are only the beginning of the cost optimization journey. It’s a journey as addressing behavior change and changing habits take time and effort. The work may appear mountainous and intimidating in the beginning. Start small and celebrate the early wins. The early successes will help build confidence and encourage behavior changes. If you enjoyed this topic, drop a comment and share ideas of what you would like to learn more about.</p> Running Duplicate Batch Jobs in HashiCorp Nomad https://crazykarlcodes.dev/posts/nomad-duplicate/ Wed, 21 Jul 2021 20:58:51 -0700[email protected] (Karl Cardenas) https://crazykarlcodes.dev/posts/nomad-duplicate/ Posts <img src="./images/nomad-duplicate/main.webp" alt="featured image" referrerpolicy="no-referrer"><p>Two approaches to injecting variability into your Nomad batch job template without having to modify the template in the future.</p> <p>This article was originally published in HashiCorp&rsquo;s blog while I was an employee. Click on the <a href="https://www.hashicorp.com/blog/running-duplicate-batch-jobs-in-hashicorp-nomad"target="_blank" rel="external nofollow noopener noreferrer">Article Link<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> to learn how to inject variability into Nomad jobs.</p> Go Lambda Cleanup https://crazykarlcodes.dev/posts/glc/ Fri, 02 Apr 2021 16:47:42 -0700[email protected] (Karl Cardenas) https://crazykarlcodes.dev/posts/glc/ Posts <img src="./images/go-lambda-cleanup/01.webp" alt="featured image" referrerpolicy="no-referrer"><figure><img src="./images/go-lambda-cleanup/01.webp" width="300" height="300"> </figure> <p>If you find yourself authoring several AWS lambdas for a serverless application architecture, you might have encountered this error:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">An error occurred: mySweetLambda– Code storage limit exceeded. </span></span><span class="line"><span class="cl"><span class="o">(</span>Service: AWSLambda<span class="p">;</span> Status Code: 400<span class="p">;</span> </span></span><span class="line"><span class="cl">Error Code: CodeStorageExceededException<span class="p">;</span> </span></span><span class="line"><span class="cl">Request ID: 05d3ae68-a7c2-a3e8-948e-41c2739638af<span class="o">)</span>.</span></span></code></pre></td></tr></table> </div> </div><p>The first time I encountered this error I wasn’t quite sure what was happening, but after some quick web searches I learned that AWS has a limit on Lambda storage that maxes out at 75Gb.</p> <p>Additionally, I also learned that AWS retains all the previous versions of all my lambdas. That’s all fine, I should probably go do some “spring cleaning” and remove the unused versions. AWS does expose the functionality to remove former versions through the console. However, in my scenario I had over 500+ versions for some of my older lambdas. Clicking through 500+ versions is not how I want to spend my time. So what options are available? Surely there has to be some better options out there.</p> <p><strong>TLDR</strong>: Visit <a href="https://github.com/karl-cardenas-coding/go-lambda-cleanup"target="_blank" rel="external nofollow noopener noreferrer">https://github.com/karl-cardenas-coding/go-lambda-cleanup<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> for simple and easy to use solution.</p> <figure><img src="./images/go-lambda-cleanup/02.webp"><figcaption> <h4>Delete Function</h4> </figcaption> </figure> <!-- *Delete function* --> <h2 class="heading-element" id="automation-is-your-friend"><span>Automation is your friend!</span> <a href="#automation-is-your-friend" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>There are a handful of open source solutions available that I stumbled upon early on in my clean up journey.</p> <ul> <li><a href="https://github.com/epsagon/clear-lambda-storage"target="_blank" rel="external nofollow noopener noreferrer">clear-lambda-storage<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> (python)</li> <li><a href="https://github.com/cloudreach/aws-lambda-es-cleanup"target="_blank" rel="external nofollow noopener noreferrer">aws-lambda-es-cleanup<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> (Cloudformation)</li> <li><a href="https://github.com/lumigo-io/SAR-Lambda-Janitor"target="_blank" rel="external nofollow noopener noreferrer">SAR-Lambda-Janitor<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> (CloudFormation)</li> <li><a href="https://www.serverless.com/plugins/serverless-prune-plugin"target="_blank" rel="external nofollow noopener noreferrer">Serverless framework prune plugin<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> (plugin)</li> </ul> <p>While these options are all great and incredible useful, something didn’t feel right about this approach. I didn’t want to install python on my system (local/CI-CD) unless I absolutely needed it. I also didn’t like the idea of having to deploy infrastructure just for the purpose of cleaning up old lambda versions.</p> <p>Ideally, I just want to run a single command that cleans up old lambdas, without having to install a language runtime, and/or requiring to setup additional infrastructure for the sole purpose of cleaning up lambdas. Is there such a solution available? The answer is <a href="https://github.com/karl-cardenas-coding/go-lambda-cleanup"target="_blank" rel="external nofollow noopener noreferrer">go-lambda-cleanup<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>!</p> <h2 class="heading-element" id="go-lambda-cleanup"><span>go-lambda-cleanup</span> <a href="#go-lambda-cleanup" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Go-lambda-cleanup is distributed as a single binary and it’s open source. The tool is available for Windows, Mac (Including M1 support), and Linux. No complicated install process, no need for additional infrastructure. Simply issue the command glc clean and the tool will start the clean up process.</p> <figure><img src="./images/go-lambda-cleanup/glc.gif" width="500" height="100"> </figure> <h2 class="heading-element" id="getting-started"><span>Getting Started</span> <a href="#getting-started" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p><a href="https://github.com/karl-cardenas-coding/go-lambda-cleanup/releases"target="_blank" rel="external nofollow noopener noreferrer">Download<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> the binary and install go-lambda-cleanup binary in a directory that is in your system’s <a href="https://superuser.com/questions/284342/what-are-path-and-other-environment-variables-and-how-can-i-set-or-use-them"target="_blank" rel="external nofollow noopener noreferrer">PATH<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. <code>/usr/local/bin</code> is the recommended path for UNIX/LINUX environments.</p> <p>Confirm glc is installed correctly by issuing the version command. If you encounter and error then this is a good indicator that the binary is not found in the system path.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">$ glc version </span></span><span class="line"><span class="cl">go-lambda-cleanup v1.0.8</span></span></code></pre></td></tr></table> </div> </div><p>To start cleaning you must have valid AWS credentials. Go-lambda-cleanup utilizes the default AWS Go SDK credentials provider to find AWS credentials.</p> <p>The <code>glc clean</code> command will by default retain all <code>$LATEST</code> versions and remove the rest.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">glc clean -r us-east-1 </span></span></code></pre></td></tr></table> </div> </div><p>If you want to retain <code>$LATEST</code> and the last two versions but remove the remaining versions, simply use the <code>-c</code> flag parameter.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">glc clean -r us-east-1 -c <span class="m">2</span></span></span></code></pre></td></tr></table> </div> </div><p>There is also a dry run option available if you want to get a preview of an actual execution.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">glc clean -r us-east-1 -d</span></span></code></pre></td></tr></table> </div> </div><figure><img src="./images/go-lambda-cleanup/03.webp" width="500" height="100"> </figure> <h2 class="heading-element" id="closing"><span>Closing</span> <a href="#closing" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>I encourage you to go checkout <a href="https://github.com/karl-cardenas-coding/go-lambda-cleanup"target="_blank" rel="external nofollow noopener noreferrer">go-lambda-cleanup<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> if you have been wanting a simpler solution to conduct AWS lambda cleanup. More details can be found in the project <a href="https://github.com/karl-cardenas-coding/go-lambda-cleanup"target="_blank" rel="external nofollow noopener noreferrer">README<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. Feel free to open op issues if you encounter/observe something odd with the tool!</p> How to use AWS DynamoDB locally… https://crazykarlcodes.dev/posts/dynamodb-local/ Thu, 20 Aug 2020 14:37:18 -0700[email protected] (Karl Cardenas) https://crazykarlcodes.dev/posts/dynamodb-local/ Posts <img src="./images/dynamodb-local/dynamodb-1.webp" alt="featured image" referrerpolicy="no-referrer"><p>Chances are most of us have unique situations for wanting to interact with DynamoDB locally, maybe it’s to develop and test different data models, perhaps it’s to develop programmatic functions to interact with the database, perhaps you want to reduce development expenses, or perhaps you’re just doing research. Regardless of your reasons, I want to help you by showing you how to leverage DynamoDB locally. We will use the following tools.</p> <p><a href="https://medium.com/faun/how-to-use-aws-dynamodb-locally-ad3bb6bd0163"target="_blank" rel="external nofollow noopener noreferrer">Medium Link<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></p> <ul> <li><a href="https://github.com/localstack/localstack"target="_blank" rel="external nofollow noopener noreferrer">Localstack<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></li> <li><a href="https://www.terraform.io/"target="_blank" rel="external nofollow noopener noreferrer">Terraform<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></li> <li><a href="https://golang.org/"target="_blank" rel="external nofollow noopener noreferrer">Go<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></li> <li><a href="https://aws.amazon.com/cli/"target="_blank" rel="external nofollow noopener noreferrer">AWS CLI<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></li> <li><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.html"target="_blank" rel="external nofollow noopener noreferrer">noSQL Workbench for DynamoDB<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></li> </ul> <p>We will walk through setting up the local environment, generating data, uploading data, interacting with the noSQL Workbench, and some neat tips to keep in mind. So with that being said, let’s dive into into it!</p> <p><strong>Note</strong>: If you get lost, simply visit <a href="https://github.com/karl-cardenas-coding/dynamodb-local-example"target="_blank" rel="external nofollow noopener noreferrer">https://github.com/karl-cardenas-coding/dynamodb-local-example<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> to view the end solution. Also, feel free to fork this template project and use it as a starting point.</p> <h2 class="heading-element" id="setting-up-the-environment"><span>Setting up the environment</span> <a href="#setting-up-the-environment" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>First thing first, ensure that you have Terraform (&gt; v0.12.0), noSQL Workbench, and localstack ( &gt; v0.11.3) installed and working on your system. If you need help installing these resources checkout the three links below. Due to the abundance of resources for getting started available, I will skip ahead and assume you have them installed.</p> <ul> <li><a href="https://learn.hashicorp.com/tutorials/terraform/install-cli"target="_blank" rel="external nofollow noopener noreferrer">Install Terraform<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></li> <li><a href="https://github.com/localstack/localstack#installing"target="_blank" rel="external nofollow noopener noreferrer">Install localstack<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></li> <li><a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/workbench.settingup.html"target="_blank" rel="external nofollow noopener noreferrer">Install noSQL Workbench for DynamoDB<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></li> </ul> <p>(<em>Alternative</em>) if you don’t want to use localstack, DynamoDB offers a <a href="https://hub.docker.com/r/amazon/dynamodb-local"target="_blank" rel="external nofollow noopener noreferrer">docker image<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>, you may use this option as well.</p> <h3 class="heading-element" id="localstack"><span>Localstack</span> <a href="#localstack" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><p>First thing first, fire up localstack. If you installed it through <code>pip</code> then it’s as easy as issuing the command <code>localstack start</code> . Or if you used the localstack docker image then it’s as simple as docker run <code>localstack/localstack</code> . If everything starts up correctly then you should be seeing something similar to the screenshot below.</p> <p><strong>Note</strong>: localstack has plenty of <a href="https://github.com/localstack/localstack#configurations"target="_blank" rel="external nofollow noopener noreferrer">parameters<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> to pass in during startup. We are taking the defaults which starts majority of the mocked AWS services but there are plenty of other options worth checking out.</p> <figure><img src="./images/dynamodb-local/dynamodb-2.webp"><figcaption> <h4>*WSL2 output through pip installation*</h4> </figcaption> </figure> <h3 class="heading-element" id="terraform"><span>Terraform</span> <a href="#terraform" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><p>Terraform is a great solution to automate the deployment of the local DynamoDB environment, along with any other AWS resources required to get the desired test environment created. In this example, we’re actually going to use Terraform to seed the database (<strong>more on that latter</strong>). However, first we need to setup Terraform to leverage localstack.</p> <p>All that is needed to leverage Terraform with localstack is to modify the aws provider block.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span><span class="lnt">24 </span><span class="lnt">25 </span><span class="lnt">26 </span><span class="lnt">27 </span><span class="lnt">28 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-terraform" data-lang="terraform"><span class="line"><span class="cl"><span class="kr">provider</span> <span class="s2">&#34;aws&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">access_key</span> <span class="o">=</span> <span class="s2">&#34;mock_access_key&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">region</span> <span class="o">=</span> <span class="s2">&#34;us-east-1&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">s3_force_path_style</span> <span class="o">=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> <span class="nx">secret_key</span> <span class="o">=</span> <span class="s2">&#34;mock_secret_key&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">skip_credentials_validation</span> <span class="o">=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> <span class="nx">skip_metadata_api_check</span> <span class="o">=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> <span class="nx">skip_requesting_account_id</span> <span class="o">=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">endpoints</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">dynamodb</span> <span class="o">=</span> <span class="s2">&#34;http://localhost:4566&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"># When using the DynomoDB local docker image </span></span></span><span class="line"><span class="cl"><span class="c1"># provider &#34;aws&#34; { </span></span></span><span class="line"><span class="cl"><span class="c1"># access_key = &#34;mock_access_key&#34; </span></span></span><span class="line"><span class="cl"><span class="c1"># region = &#34;us-east-1&#34; </span></span></span><span class="line"><span class="cl"><span class="c1"># secret_key = &#34;mock_secret_key&#34; </span></span></span><span class="line"><span class="cl"><span class="c1"># skip_credentials_validation = true </span></span></span><span class="line"><span class="cl"><span class="c1"># skip_metadata_api_check = true </span></span></span><span class="line"><span class="cl"><span class="c1"># skip_requesting_account_id = true </span></span></span><span class="line"><span class="cl"><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"># endpoints { </span></span></span><span class="line"><span class="cl"><span class="c1"># dynamodb = &#34;http://localhost:8000&#34; </span></span></span><span class="line"><span class="cl"><span class="c1"># } </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="err">#</span> <span class="p">}</span></span></span></code></pre></td></tr></table> </div> </div><p>If you review the code snippet above you will probably notice how on line 10 we are specifying a code block for <code>endpoints</code>. This is where we essentially point Terraform to <code>localhost</code> and the port that localstack is listening on, for the respective mocked AWS service. I have also added the DynomoDB docker image configuration for those of you who took that approach, just remember to ensure that the container port specified is correct.</p> <p>In the example project I <a href="https://github.com/karl-cardenas-coding/dynamodb-local-example"target="_blank" rel="external nofollow noopener noreferrer">provided<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>, take a peek at the <code>main.tf</code> <a href="https://github.com/karl-cardenas-coding/dynamodb-local-example/blob/master/main.tf#L4-L52"target="_blank" rel="external nofollow noopener noreferrer">file<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. In the example project, a customer order table is being deployed. In addition, I have a <code>local secondary index</code> and <code>global secondary index</code>.</p> <p>Let’s deploy this Terraform configuration.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-tf" data-lang="tf"><span class="line"><span class="cl"><span class="nx">terraform</span> <span class="nx">init</span> <span class="o">&amp;&amp;</span> <span class="nx">terraform</span> <span class="nx">plan</span> <span class="o">-</span><span class="nx">out</span><span class="o">=</span><span class="s2">&#34;myplan&#34;</span></span></span></code></pre></td></tr></table> </div> </div><figure><img src="./images/dynamodb-local/dynamodb-3.webp"><figcaption> <h4>**What Terraform plan should reveal**</h4> </figcaption> </figure> <p>If you get a <strong>similar</strong> output as the picture above, go ahead and issue the command below</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform apply -auto-approve myplan</span></span></code></pre></td></tr></table> </div> </div><figure><img src="./images/dynamodb-local/dynamodb-4.webp"><figcaption> <h4>**Successful terraform apply output**</h4> </figcaption> </figure> <p>Let us validate that we actually have a table in localstack. We can leverage the AWS CLI for this. We are expecting a table by the name of <code>shipping-south-america</code></p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">aws dynamodb list-tables --endpoint-url http://localhost:4566</span></span></code></pre></td></tr></table> </div> </div><p>If you got the following output (see below), then you did everything correctly.</p> <p>The key thing to remember is when using the AWS CLI with localstack OR the DynamoDB docker image, is to leverage the <code>--endpoint-url</code> parameter.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$ aws dynamodb list-tables --endpoint-url http://localhost:4566 </span></span><span class="line"><span class="cl"><span class="o">{</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;TableNames&#34;</span>: <span class="o">[</span> </span></span><span class="line"><span class="cl"> <span class="s2">&#34;shipping-south-america&#34;</span> </span></span><span class="line"><span class="cl"> <span class="o">]</span> </span></span><span class="line"><span class="cl"><span class="o">}</span></span></span></code></pre></td></tr></table> </div> </div><p>So we now have a mocked DynamoDB table, great! But it’s no good without data, unless you are only planning on working on putting items into the table. So let’s add mock data.</p> <h3 class="heading-element" id="adding-data-to-dynamodb"><span>Adding Data to DynamoDB</span> <a href="#adding-data-to-dynamodb" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><p>When it comes to generating mock data I prefer <a href="https://www.mockaroo.com/"target="_blank" rel="external nofollow noopener noreferrer">Mockaroo<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> (<strong>free</strong>). It makes it super easy to come up with all kinds of mock data and at a high volume, plus they offer free mock API endpoints. Seriously, such a neat resource! But I digress. By leveraging Mockarro, I have generated 50 JSON objects that look similar to this:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span><span class="lnt">8 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"><span class="nt">&#34;orderId&#34;</span><span class="p">:</span> <span class="s2">&#34;9109737812&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="nt">&#34;customerId&#34;</span><span class="p">:</span> <span class="s2">&#34;70-774-5972&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="nt">&#34;shipped&#34;</span><span class="p">:</span> <span class="s2">&#34;12/4/2019&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="nt">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;[email protected]&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="nt">&#34;address&#34;</span><span class="p">:</span> <span class="s2">&#34;50503 Corben Plaza&#34;</span><span class="p">,</span> </span></span><span class="line"><span class="cl"><span class="nt">&#34;cost&#34;</span><span class="p">:</span> <span class="s2">&#34;$97.21&#34;</span> </span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></td></tr></table> </div> </div><p>The raw JSON content is in a file named <code>raw-data.json</code>. However, we have a problem. We simply cannot upload JSON objects to DynamoDB, we have to convert the JSON content to DynamoDB JSON (yes, that’s a thing).</p> <p>DynamoDB expects the JSON to be in the following format.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;orderId&#34;</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;S&#34;</span><span class="p">:</span> <span class="s2">&#34;8397763392&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;customerId&#34;</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;S&#34;</span><span class="p">:</span> <span class="s2">&#34;92-084-2567&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;email&#34;</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;S&#34;</span><span class="p">:</span> <span class="s2">&#34;[email protected]&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;address&#34;</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;S&#34;</span><span class="p">:</span> <span class="s2">&#34;08207 Ronald Regan Parkway&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">},</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;cost&#34;</span><span class="p">:</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nt">&#34;S&#34;</span><span class="p">:</span> <span class="s2">&#34;$23.66&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></td></tr></table> </div> </div><p>You’re probably thinking, <em>Ooofff</em>. This could be tedious to do for large datasets. Currently, the AWS DynamoDB Console and AWS CLI does not offer the ability to import data from a JSON file. The alternative is using the AWS SDK.</p> <p>Normally, I would recommend using a simple script that reads in the JSON file and uploads it. I have provided a <a href="https://github.com/karl-cardenas-coding/dynamodb-local-example/tree/master/upload-tool"target="_blank" rel="external nofollow noopener noreferrer">Go script<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> for those of you that want a working example of using such a script. Feel free to leverage this solution for your own need. Instructions can be found in the README. This is hands down the better approach.</p> <p>However, if you feel uncomfortable using the provided Go script, or perhaps rather just work with the AWS CLI then you can use the approach I will provide below.</p> <p>You can convert JSON objects to DynamoDB objects using the free tool, <a href="https://dynobase.dev/dynamodb-json-converter-tool/"target="_blank" rel="external nofollow noopener noreferrer">DynamoDB JSON Converter<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. You will however have to change the data format to conform to the format the AWS CLI expects, take a look at the formatted-data.json file in the static folder. The key is to remember that the table name must be the first property and that every object must be wrapped inside an <code>Item {}</code>. It’s easy to change regular JSON to this format with the help of the <a href="https://dynobase.dev/dynamodb-json-converter-tool/"target="_blank" rel="external nofollow noopener noreferrer">DynamoDB JSON Converter<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> tool but it is tedious work. Be aware that using the AWS CLI limits you to 25 objects when using the <code>batch-write-item</code> command. That’s why the Go script provided is a better solution as it has no upload limitation.</p> <p>The next step is having Terraform upload the data, either through the AWS CLI or through the Go binary. Let’s take a peek at how that is accomplished.</p> <p><strong>Note</strong>: If you are using the example project, make sure you uncomment ONLY one of the <code>null_resources</code> . Pick the solution you prefer but if you go with the Go script make sure to compile the binary (see <a href="https://github.com/karl-cardenas-coding/dynamodb-local-example"target="_blank" rel="external nofollow noopener noreferrer">README<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> for directions).</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span><span class="lnt">15 </span><span class="lnt">16 </span><span class="lnt">17 </span><span class="lnt">18 </span><span class="lnt">19 </span><span class="lnt">20 </span><span class="lnt">21 </span><span class="lnt">22 </span><span class="lnt">23 </span><span class="lnt">24 </span><span class="lnt">25 </span><span class="lnt">26 </span><span class="lnt">27 </span><span class="lnt">28 </span><span class="lnt">29 </span><span class="lnt">30 </span><span class="lnt">31 </span><span class="lnt">32 </span><span class="lnt">33 </span><span class="lnt">34 </span><span class="lnt">35 </span><span class="lnt">36 </span><span class="lnt">37 </span><span class="lnt">38 </span><span class="lnt">39 </span><span class="lnt">40 </span><span class="lnt">41 </span><span class="lnt">42 </span><span class="lnt">43 </span><span class="lnt">44 </span><span class="lnt">45 </span><span class="lnt">46 </span><span class="lnt">47 </span><span class="lnt">48 </span><span class="lnt">49 </span><span class="lnt">50 </span><span class="lnt">51 </span><span class="lnt">52 </span><span class="lnt">53 </span><span class="lnt">54 </span><span class="lnt">55 </span><span class="lnt">56 </span><span class="lnt">57 </span><span class="lnt">58 </span><span class="lnt">59 </span><span class="lnt">60 </span><span class="lnt">61 </span><span class="lnt">62 </span><span class="lnt">63 </span><span class="lnt">64 </span><span class="lnt">65 </span><span class="lnt">66 </span><span class="lnt">67 </span><span class="lnt">68 </span><span class="lnt">69 </span><span class="lnt">70 </span><span class="lnt">71 </span><span class="lnt">72 </span><span class="lnt">73 </span><span class="lnt">74 </span><span class="lnt">75 </span><span class="lnt">76 </span><span class="lnt">77 </span><span class="lnt">78 </span><span class="lnt">79 </span><span class="lnt">80 </span><span class="lnt">81 </span><span class="lnt">82 </span><span class="lnt">83 </span><span class="lnt">84 </span><span class="lnt">85 </span><span class="lnt">86 </span><span class="lnt">87 </span><span class="lnt">88 </span><span class="lnt">89 </span><span class="lnt">90 </span><span class="lnt">91 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-terraform" data-lang="terraform"><span class="line"><span class="cl"><span class="c1">############################# </span></span></span><span class="line"><span class="cl"><span class="c1"># DynamoDB </span></span></span><span class="line"><span class="cl"><span class="c1">############################# </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">resource</span> <span class="s2">&#34;aws_dynamodb_table&#34;</span> <span class="s2">&#34;main-table&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">name</span> <span class="o">=</span> <span class="nb">var</span><span class="p">.</span><span class="nx">table</span><span class="na">-name</span> </span></span><span class="line"><span class="cl"> <span class="nx">billing_mode</span> <span class="o">=</span> <span class="s2">&#34;PROVISIONED&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">read_capacity</span> <span class="o">=</span> <span class="m">2</span> </span></span><span class="line"><span class="cl"> <span class="nx">write_capacity</span> <span class="o">=</span> <span class="m">2</span> </span></span><span class="line"><span class="cl"> <span class="nx">hash_key</span> <span class="o">=</span> <span class="s2">&#34;orderId&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">range_key</span> <span class="o">=</span> <span class="s2">&#34;customerId&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">stream_enabled</span> <span class="o">=</span> <span class="kc">true</span> </span></span><span class="line"><span class="cl"> <span class="nx">stream_view_type</span> <span class="o">=</span> <span class="s2">&#34;NEW_IMAGE&#34;</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">attribute</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">name</span> <span class="o">=</span> <span class="s2">&#34;orderId&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">type</span> <span class="o">=</span> <span class="s2">&#34;S&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">attribute</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">name</span> <span class="o">=</span> <span class="s2">&#34;customerId&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">type</span> <span class="o">=</span> <span class="s2">&#34;S&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">attribute</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">name</span> <span class="o">=</span> <span class="s2">&#34;shipped&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">type</span> <span class="o">=</span> <span class="s2">&#34;S&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">local_secondary_index</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">name</span> <span class="o">=</span> <span class="s2">&#34;lsi-orderId-customerId&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">range_key</span> <span class="o">=</span> <span class="s2">&#34;customerId&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">projection_type</span> <span class="o">=</span> <span class="s2">&#34;ALL&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">global_secondary_index</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">name</span> <span class="o">=</span> <span class="s2">&#34;gsi-shipped&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">hash_key</span> <span class="o">=</span> <span class="s2">&#34;orderId&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">range_key</span> <span class="o">=</span> <span class="s2">&#34;shipped&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">write_capacity</span> <span class="o">=</span> <span class="m">1</span> </span></span><span class="line"><span class="cl"> <span class="nx">read_capacity</span> <span class="o">=</span> <span class="m">1</span> </span></span><span class="line"><span class="cl"> <span class="nx">projection_type</span> <span class="o">=</span> <span class="s2">&#34;ALL&#34;</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"> <span class="nx">tags</span> <span class="o">=</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">Name</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nb">var</span><span class="p">.</span><span class="nx">table</span><span class="na">-name</span><span class="si">}</span><span class="s2">-</span><span class="si">${</span><span class="nb">var</span><span class="p">.</span><span class="nx">environment</span><span class="si">}</span><span class="s2">&#34;</span> </span></span><span class="line"><span class="cl"> <span class="nx">Environment</span> <span class="o">=</span> <span class="nb">var</span><span class="p">.</span><span class="nx">environment</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"><span class="p">}</span><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"># ############################ </span></span></span><span class="line"><span class="cl"><span class="c1"># Execut AWS CLI script </span></span></span><span class="line"><span class="cl"><span class="c1"># ############################ </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">resource</span> <span class="s2">&#34;null_resource&#34;</span> <span class="s2">&#34;init-db&#34;</span> <span class="p">{</span><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> // This will cause the upload script to only execute when the table changes id (recreate). </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">triggers</span> <span class="o">=</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">new</span> <span class="o">=</span> <span class="nx">aws_dynamodb_table</span><span class="p">.</span><span class="nx">main</span><span class="o">-</span><span class="nx">table</span><span class="p">.</span><span class="nx">id</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">provisioner</span> <span class="s2">&#34;local-exec&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">command</span> <span class="o">=</span> <span class="o">&lt;&lt;EOT</span><span class="s"> </span></span></span><span class="line"><span class="cl"><span class="s"> aws dynamodb batch-write-item --request-items file://static/formatted-data.json --endpoint-url http://localhost:4566 </span></span></span><span class="line"><span class="cl"><span class="s"> </span><span class="o">EOT</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">depends_on</span> <span class="o">=</span> <span class="p">[</span><span class="nx">aws_dynamodb_table</span><span class="p">.</span><span class="nx">main</span><span class="o">-</span><span class="nx">table</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="p">}</span><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1">########################## </span></span></span><span class="line"><span class="cl"><span class="c1"># Execut Go binary script </span></span></span><span class="line"><span class="cl"><span class="c1">########################### </span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="kr">resource</span> <span class="s2">&#34;null_resource&#34;</span> <span class="s2">&#34;init-db-go&#34;</span> <span class="p">{</span><span class="c1"> </span></span></span><span class="line"><span class="cl"><span class="c1"> // This will cause the upload script to only execute when the table changes id (recreate). </span></span></span><span class="line"><span class="cl"><span class="c1"></span> <span class="nx">triggers</span> <span class="o">=</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">new</span> <span class="o">=</span> <span class="nx">aws_dynamodb_table</span><span class="p">.</span><span class="nx">main</span><span class="o">-</span><span class="nx">table</span><span class="p">.</span><span class="nx">id</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">provisioner</span> <span class="s2">&#34;local-exec&#34;</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">command</span> <span class="o">=</span> <span class="o">&lt;&lt;EOT</span><span class="s"> </span></span></span><span class="line"><span class="cl"><span class="s"> ./upload-logic </span></span></span><span class="line"><span class="cl"><span class="s"> </span><span class="o">EOT</span> </span></span><span class="line"><span class="cl"> <span class="nx">environment</span> <span class="o">=</span> <span class="p">{</span> </span></span><span class="line"><span class="cl"> <span class="nx">TABLE_NAME</span> <span class="o">=</span> <span class="nx">aws_dynamodb_table</span><span class="p">.</span><span class="nx">main</span><span class="o">-</span><span class="nx">table</span><span class="p">.</span><span class="nx">id</span> </span></span><span class="line"><span class="cl"> <span class="nx">REGION</span> <span class="o">=</span> <span class="nb">var</span><span class="p">.</span><span class="nx">region</span> </span></span><span class="line"><span class="cl"> <span class="nx">DYNAMODB_ADDR</span> <span class="o">=</span> <span class="nb">var</span><span class="p">.</span><span class="nx">dynamodb</span><span class="o">-</span><span class="nx">addr</span> </span></span><span class="line"><span class="cl"> <span class="nx">JSON_PATH</span> <span class="o">=</span> <span class="nb">var</span><span class="p">.</span><span class="nx">json</span><span class="o">-</span><span class="nb">file</span><span class="o">-</span><span class="nx">path</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="p">}</span> </span></span><span class="line"><span class="cl"> <span class="nx">depends_on</span> <span class="o">=</span> <span class="p">[</span><span class="nx">aws_dynamodb_table</span><span class="p">.</span><span class="nx">main</span><span class="o">-</span><span class="nx">table</span><span class="p">]</span> </span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></td></tr></table> </div> </div><p>Let’s upload the data by issuing the commands below.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform init <span class="o">&amp;&amp;</span> terraform plan -out<span class="o">=</span><span class="s2">&#34;myplan&#34;</span> </span></span><span class="line"><span class="cl">terraform apply -auto-approve myplan</span></span></code></pre></td></tr></table> </div> </div><p>Validate that you have data in the table by issuing the following command. Don’t forget to use the table name you provided Terraform.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">aws dynamodb scan --table-name shipping-south-america --endpoint-url http://localhost:4566</span></span></code></pre></td></tr></table> </div> </div><p>If you see your data in the console output then you have done everything correctly and can move onto the next step of using the NoSQL WorkBench for DynamoDB 👏.</p> <h2 class="heading-element" id="nosql-workbench-for-dynamodb"><span>noSQL WorkBench for DynamoDB</span> <a href="#nosql-workbench-for-dynamodb" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>This tool is great for interacting with DynamoDB in a development/test environment. It’s really handy when you are trying to identify the proper data model for the table. It comes with a few example data models out of the box. Use these as a learning tool when learning and/or researching what NoSQL data model to use. There are many reasons why you would want to leverage NoSQL Workbench for DynamoDB, to name a few.</p> <ul> <li>Identifying/Creating Queries</li> <li>Adding Items</li> <li>Removing Items</li> <li>Evaluate Indexes usage (Local and Secondary)</li> <li>Example code for programmatic function (Java, Python, JavaScript)</li> </ul> <p>However, before we can utilize any of these neat functions we need to point NoSQL Workbench for DynamoDB to our mocked table. So let’s get started.</p> <p>Open up noSQL and click on “Operation builder” (<em>left-hand side navbar</em>)</p> <figure><img src="./images/dynamodb-local/dynamodb-5.webp"><figcaption> <h4>*Select Operation builder*</h4> </figcaption> </figure> <figure><img src="./images/dynamodb-local/dynamodb-6.webp"> </figure> <p>This will take you to yet another screen. Click on the “DynamoDB local” tab and fill out the information required. You should only have to provide a name for this connection and the local port that the mocked DynamoDB is listening on.</p> <p>If you are using localstack then it will be port <code>4566</code> but if you are using a Docker image then use the container port, usually <code>8000</code> . Click on the blue button named “Connect”.</p> <figure><img src="./images/dynamodb-local/dynamodb-7.webp"><figcaption> <h4>*The configuration step for setting up local usage*</h4> </figcaption> </figure> <p>After clicking on “Connect”, you should be on the screen before the configurations steps. Ensure you see your “localhost” connection and click on the “Open” button.</p> <figure><img src="./images/dynamodb-local/dynamodb-8.webp"><figcaption> <h4>*Opening up the localhost connection*</h4> </figcaption> </figure> <p>If your local mocked environment is up and running then you should be taken to a screen that has your table information. You will have to click on your table’s name in order to see the table content (see image below).</p> <figure><img src="./images/dynamodb-local/dynamodb-9.webp"><figcaption> <h4>*All of our table data*</h4> </figcaption> </figure> <p><em>Voila</em>! There you have all of your data available in a nice and clean user interface. A lot of the AWS Console functionality is available in this tool, such as removing an item, modifying an item, adding an item, scans, query, and more. It’s nice to have the option of conducting manual actions versus having to use the AWS CLI or being forced to leverage a programmatic function (<em>when developing</em>). I won’t go into too much more detail about NoSQL Workbench for DynamoDB but before I wrap up this article I do want to share with you this awesome gem that the tool provides 💎.</p> <h2 class="heading-element" id="sdk-help"><span>SDK Help</span> <a href="#sdk-help" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>When using the AWS DynamoDB SDK, regardless of the language, it takes a bit to learn how to interact with DynamoDB. DynamoDB has its own unique quirks and opinionated interactions when using its API.</p> <p>It’s for these situations where NoSQL Workbench for DynamoDB is extremely useful. Let’s say you want to create a function that queries DynamoDB with a condition check and filter. We want to know if orderID <code>6076643781</code> has been shipped.</p> <p>Let’s leverage the query functionality of this tool. Click on “Build Operations” — -&gt; “Query”.</p> <figure><img src="./images/dynamodb-local/dynamodb-10.webp"><figcaption> <h4>*The query populated*</h4> </figcaption> </figure> <p>In the image above I have filled out the required information to identify this information. If I click on the “Execute” button, it will take me back to the table view and present the results of the query. If there are no results then the screen will be blank and a little pop up will notify us of no results found. This is super useful when learning and identifying the access patterns for your database. It also comes in handy when deciding on what type of indexes to leverage.</p> <p>You might have also noticed that in the image above that there is a “Generate code” button. Click on it! You should be seeing the generated code</p> <p><figure><img src="./images/dynamodb-local/dynamodb-11.webp"> </figure> <em>Ah yeah, completed code!</em></p> <p>Now you have the query logic available to you. All you need to do is pick the language of your choice! This is an amazing time saver but also super handy when learning how to use the DynamoDB SDK and interacting with DynamoDB’s API. This functionality works for all “Build Operations” that the tool provides.</p> <p>To reset the table view, simply hit the “Scan” button.</p> <p>There is more to this tool but the last trick is perhaps one of the best functionality that comes out of the box with NoSQL Workbench for DynamoDB.</p> <h2 class="heading-element" id="clean-up"><span>Clean Up</span> <a href="#clean-up" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>To clean up, simply use Terraform and provide the command below.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">terraform destroy -auto-approve</span></span></code></pre></td></tr></table> </div> </div><p>Stop localstack (Ctrl +C) and/or the Docker container that you spun up for DynamoDB.</p> <p>That’s a wrap! The three tools combined, localstack, Terraform, and NoSQL Workbench for DynamoDB make up the perfect local DynamoDB development experience. It’s pretty much all you need when interacting with DynamoDB in a local setting. You now don’t have to worry about messing up the team’s development instance or corrupting the data. You now have your very own DynamoDB table that you can use however you want to and it’s free.</p> <p>Again, feel free to use my template project as a starting point, simply use the template button and get started. All I ask is that you pay it forward by helping someone else out in the future 😉</p> <p><a href="https://github.com/karl-cardenas-coding/dynamodb-local-example"target="_blank" rel="external nofollow noopener noreferrer">https://github.com/karl-cardenas-coding/dynamodb-local-example<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></p> <p><em>Thanks for reading!</em></p> Using a Community Support Model https://crazykarlcodes.dev/posts/community/ Thu, 20 Aug 2020 13:34:00 -0700[email protected] (Karl Cardenas) https://crazykarlcodes.dev/posts/community/ Posts <img src="./images/community/community.webp" alt="featured image" referrerpolicy="no-referrer"><p>It’s tough to find an organization that is not leveraging a public cloud platform. With all the SaaS, PaaS, and IaaS providers out there, chances are you’ve used cloud services in some manner. As public cloud utilization continues to ramp up in many industries (e.g., insurance, financial, business), organizations are encountering a new challenge: how to correctly consume these new platforms and technologies. <strong>It’s a workforce challenge more so than it is a technical challenge</strong>.</p> <blockquote> <p>This article was originally published in the State Farm Engineering Blog. Link to the original blog can be found <a href="https://engineering.statefarm.com/blog/community"target="_blank" rel="external nofollow noopener noreferrer">here<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>.</p></blockquote> <p>It’s a workforce challenge because, at the end of the day, it comes down to technical education and experience. Organizations with a workforce skilled in cloud technologies will have a smoother cloud consumption experience compared to one lacking a workforce with cloud experience. Every organization is competing for talent - we all want that DevSecOps engineer and/or Cloud Architect that can do it all (CI/CD, Programming, Security, Architecting, Data, Test, etc.) - but truth to be told, those individuals are rare to come by, let alone retain. Ideally, placing these individuals among inexperienced teams would be the best approach. They could act as a mentor and guide colleagues through successful application deployments. However, this option is not really scalable for large enterprises that are starting out their cloud journey. So what is the alternative? The Community Support model!</p> <h2 class="heading-element" id="what-is-the-community-support-model"><span>What is the Community Support model?</span> <a href="#what-is-the-community-support-model" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>At State Farm® our public cloud team embraces and continuously invests in the Community Support model. Our belief is that in order to succeed as an organization and at scale, we have to come together as a community and help one another. A big contributor to this mentality is our organization’s <a href="https://www.statefarm.com/about-us/company-overview/company-profile/mission"target="_blank" rel="external nofollow noopener noreferrer">shared values<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> of helping those in need.</p> <p>State Farm is only three years into its public cloud journey and already has observed positive results from this model.</p> <p>At State Farm, we define the Community Support model as the following:</p> <p><em>The promotion of - and reliance on - knowledge sharing for both failures and successes, so that teams may learn from the past in order to succeed at scale in technical endeavors.</em></p> <p>There is a lot packed into this definition, but the main takeaway is that individuals and teams need to help each other by leveraging their expertise and sharing their failures and successes. This not only aids in preventing unnecessary duplication and resource waste (time and money), but it also prevents teams downstream that might find themselves in a similar position from having to start over. Other teams can leverage the lessons learned and reapply architectures and reusable code configurations (more on this later).</p> <p>This is not a novel concept, as history can attest. The Community Support model provides great results time and again. Groups of individuals are a much more potent force when united.</p> <figure><img src="./images/community/one.webp"> </figure> <p>Some of you might be asking: “Where is your Cloud Center of Excellence (CCoE)?” State Farm has a public cloud platform enablement team that fulfills most of the traditional <a href="https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/organize/cloud-center-of-excellence"target="_blank" rel="external nofollow noopener noreferrer">CCoE<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> duties and responsibilities required to support a platform. However, we don’t believe that reliance on one team to solve all of the technical challenges platform consumers might experience is the best approach. Instead, we leverage the platform team for the following (not limited to):</p> <p>Platform (Cloud Service Providers) ownership and support</p> <ul> <li>Tackle platform level challenges (technical and non-technical)</li> <li>Enable shared services</li> <li>Provide technical patterns</li> <li>Improve process and reduce friction encountered by product teams</li> <li>Mitigate risk</li> <li>Foster a technical community</li> <li>Provide guidance and technical expertise (<strong>limited by team bandwidth</strong>)</li> </ul> <p>The last bullet and the text in bold &ldquo;<strong>limited by team bandwidth</strong>&rdquo; is why State Farm is an adopter of the Community Support model. We believe in empowering teams to identify the best technical solution for their needs. By empowering teams to create innovative solutions, and not being prescriptive in terms of architecture, our early adopters have created amazing results. Architectural patterns are provided by the platform enablement team and the community of public cloud platform consumers, and lessons learned from the “boots on the ground” are harnessed and distributed through different mediums such as presentations and wiki entries.</p> <h2 class="heading-element" id="building-the-community"><span>Building the Community</span> <a href="#building-the-community" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p><em>Building and fostering a community is hard work! Some ideas will work, others will fail. The key is to remember that this is a long-term solution and to not dwell on the short-term wins and losses.</em></p> <p>As a preface to the discussion below, the above statement highlights the fact that, there is no set path to creating a community. Every organization is different and its culture must be taken into consideration. Allow State Farm to share its journey.</p> <h3 class="heading-element" id="year-one"><span>Year One</span> <a href="#year-one" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><p>At State Farm, the public cloud community started out in our internal technical forum (similar to Slack). As the public cloud platform was being stood up, we immediately established a strong presence in various internal State Farm communication channels. This was done by creating public cloud specific forums for technical employees to ask questions. In the early days of this channel, the platform team was the main responder to questions. However, as time progressed and people started to gain experience with the platform, others started to chime in and help answer questions from various platform consumers. <strong>We took note of those individuals outside of the platform team that were constantly helping others and being positive role models</strong>. In addition, we allowed people to have fun in these channels to incorporate a positive spirit where possible. Taking the time to have fun is important. When you see others outside of your team starting to help on a regular basis, that’s a good indication of the community seed growing.</p> <p>To celebrate our regular participants, we created a &ldquo;<strong>Builder of the Month</strong>&rdquo; award. Every month the platform team nominates an outstanding community member that was helping other platform consumers. The Builder of the Month’s name is displayed on a banner in the internal public cloud forum. In addition, a thank you note is sent to the nominee’s leadership, with a small monetary award presented to the employee as a bonus. The “thank you” note goes a long way, especially when it’s sent to the receipient’s leadership so that they are aware of their employee’s positive contribution, even if they are not active in the technical forums.</p> <p>In parallel, as the technical forum community was kicking off, another grassroots effort began under the leadership of a motivated analyst - <strong>a certification study group</strong>. This study group started out with about five individuals meeting every week to study for an AWS certification. Each member would present on a topic they were studying for, such as EC2, IAM, etc. As people caught word of this study group, others started to join, to the point where it’s now a group composed of 1100+ members.</p> <figure><img src="./images/community/two.webp"> </figure> <h3 class="heading-element" id="year-two"><span>Year Two</span> <a href="#year-two" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h3><p>State Farm supported the public cloud enablement team and their public cloud adoption efforts from day one. After witnessing how the public cloud community was supporting each other via a distributed knowledge model, <strong>the decision to stand up a new team was made, along with the addition of a technical leader who was passionate in supporting the community</strong>. At this point, State Farm had officially acknowledged the Community Support model, which started out as a grassroots effort. Two talented Software Developers were added to this team and assigned the role of Developer Evangelist/Cloud Evangelist. Their primary role is to support the community (technical and non-technical) and address issues encountered by the platform consumers.</p> <p>Several efforts were kicked off after the official support for the community model:</p> <h4 class="heading-element" id="community-champions"><span>Community Champions</span> <a href="#community-champions" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h4><p>Those individuals noticed by the platform enablement team for constantly helping others and being positive role models were now officially acknowledged (both leaders and analysts). The formal recognition was announced to a large audience of more than 600+ people during the monthly scheduled public cloud platform showcase event. These individuals are continually engaged for consultation, and are critical to help sustain the community. Their voices also carry weight when raising concerns to the platform enablement team. These community leaders are the key to expanding the Community Support model.</p> <h4 class="heading-element" id="public-cloud-summits"><span>Public Cloud Summits</span> <a href="#public-cloud-summits" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h4><p>A State Farm internal mini-conference for public cloud platform consumers within a geographical location, the public cloud summit event lasts for two days and introduces various topics, such as security, data, application, ci/cd, data, community, and infrastructure. The main goal is to share knowledge with one another, but it also allows the platform enablement team to meet with internal platform customers and build relationships. Brain storming sessions and action items to improve the platform, process, and the community, are takeaways from these summits. Only two summits are being targeted a year.</p> <h4 class="heading-element" id="public-cloud-community-challenges"><span>Public Cloud Community Challenges</span> <a href="#public-cloud-community-challenges" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h4><p>The Public Cloud Community Challenge is a monthly event hosted by the State Farm public cloud platform enablement team. The goal is to promote community participation and help the community invest in itself. Challenges range from technical to non-technical, with the intention of solving common community experience pain points. Those who earn 1st, 2nd, or 3rd place receive small monetary awards.</p> <p>Amazing products have resulted from these challenges. To mention a few:</p> <ul> <li> <p>S3 Storage Lifecycle Calculator - A UI tool that helps users understand the cost of S3 and which storage lifecycle class to apply for cost optimization.</p> </li> <li> <p>Public Cloud CLI - A command line interface tool that removes the burden of configuring common tools used for public cloud work (Terraform, AWS CLI, credentials configuration, etc). This tool reduces the time spent configuring workstations from the upwards of 2hrs down to minutes. The ROI on this solution easily outweighs the small amount invested in the monetary award. It also solved a critical pain point experienced by the platform consumers thus improving the customer experience.</p> </li> <li> <p><a href="https://github.com/circa10a/go-aws-news"target="_blank" rel="external nofollow noopener noreferrer">AWS News<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> - A tool that fetches daily updates/releases from AWS and sends notifications to social sites. This allows our platform consumers to stay aware of new functionality.</p> </li> <li> <p>Training Labs - Labs that platform consumers can execute in training environments to further their technical education with a hands-on approach.</p> </li> <li> <p>Terraform Module for S3 - A module that simplifies the complexity of deploying S3 with all required tags and lifecycle policies required by platform controls.</p> </li> </ul> <h4 class="heading-element" id="public-cloud-guild"><span>Public Cloud Guild</span> <a href="#public-cloud-guild" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h4><p>The Public Cloud Guild is for State Farm public cloud platform users that want to dive deeper into technical topics, share development experiences, and provide a certain level of oversight for infrastructure patterns. This is a scheduled monthly event that is open to all employees and that lasts an hour. The time is leveraged to share lessons learned, new architectures, and new tools. Attendance is usually upwards of 250 participants.</p> <h4 class="heading-element" id="cloud-pains"><span>Cloud Pains</span> <a href="#cloud-pains" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h4><p>This is a git repository where platform consumers can open up issues that they want the platform enablement team to investigate/address. This allows the public cloud platform enablement team to prioritize work while remaining transparent with platform consumers. A lot of great discussion occur under the various issues comment sections, which helps the team steer towards an optimal solution.</p> <h2 class="heading-element" id="lessons-learned"><span>Lessons Learned</span> <a href="#lessons-learned" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>As mentioned earlier, some ideas will work better than others. It’s important to conduct review sessions to identify why something didn’t work, and what different choices could had been made. Here are some lessons learned:</p> <p><strong>Community Challenges</strong> - The community challenges have generated some amazing resources, but despite these successes we are still encountering low participation among our platform consumers. The non-technical challenges have the greatest participation. Having a monthly challenge tied with monetary awards is not sufficient to incentivize enough participation. The Community Challenge might be better used as a tactical tool to address technical challenges, as witnessed by the generated solutions. A reevaluation of the community challenges is currently underway.</p> <p><strong>Certification Study Group</strong> - The certification study group is a success story. The volunteer presentation approach works well and the group does a great job celebrating the success of its members when they pass an exam. In addition, the certification study group now has a mentor/mentee program where a mentor meets with its mentees once a week to help them with their certification exam preparation. The certification study group has received positive praises from State Farm executive leadership.</p> <p><strong>Changing Culture</strong> - If you want to see change you have to start with yourself, eventually others will follow, and before you know it, you have a community. Knowledge sharing practices are still not at the point to where they need to be. The goal is for this to become muscle memory amongst the workforce. As a platform enablement team, it’s our responsibility to ensure that there are channels available for knowledge sharing. Once those channels are present, the challenge shifts to identifying presenters and content. Monitoring the technical forums aids greatly in identifying presenters for the Public Cloud Guild.</p> <h2 class="heading-element" id="next-steps"><span>Next Steps</span> <a href="#next-steps" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>We are now entering the third year of the Community Support model. The following efforts are currently underway:</p> <p><strong>Marketing</strong> - Increased focus on raising awareness about the community and the resources generated by it. The following marketing materials were created: community posters outlining resources available, certification study group stickers, community champions stickers, and stickers for those delivering internal public cloud related presentations.</p> <p><strong>AWS DeepRacer Event</strong> - This community event challenges teams to compete against one another using AWS DeepRacer. This event was postponed to a later date due to COVID-19.</p> <p><strong>Cloud My App</strong> - An informal interview style of walking through an architecture that is in production. This is inspired by Amazon’s This is My Architecture.</p> <p><strong>Public Cloud Resource Website</strong> - A digital experience that will act as the starting point for onboarding, but also as a reference point for platform consumers, targeting the following topics: education, patterns, community resources, process guidance and more. The goal is to address the challenges encountered with hundreds of internal wiki pages.</p> <p><strong>Analytics/Metrics</strong> - Efforts are currently underway to better understand the patterns of participation among our community members. This includes gathering metrics for technical forums (visits, comments, views, and replies), page views for community resources, and download counts for community tools. The goal is to generate a mathematical model for calculating ROI pertaining to community efforts.</p> <p><strong>External Knowledge Sharing</strong> - In order to help other organizations that might encounter the same challenges we have tackled in the past, we are also promoting external knowledge sharing. It’s our way of paying it forward and contributing back to the technology community. Recent examples of external knowledge sharing from State Farm include:</p> <ul> <li>TechWell Conference</li> <li>THINK Conference</li> <li>DevOps Enterprise Summit</li> <li>Cloud Foundry Summit</li> <li>ADDO</li> <li>HashiConf</li> <li>Dreamforce</li> <li>Gitlab Commit Conference</li> <li>Financial Services Exchange</li> </ul> <h2 class="heading-element" id="closing"><span>Closing</span> <a href="#closing" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>The idea of creating a community can be daunting, especially if you look at the mountain of work ahead of you. Our advice is to start small, ignore the mountain, and focus on taking things one step at a time. You, the reader, have the power to start a community. All that is required is for someone to start. We mentioned earlier that if you want to see change, start with yourself. Once you have a follower, the rest will take care of itself (think rolling snowball effect). Try a few things and see what works.</p> How to Create a Digital Portfolio That Is Free and Serverless https://crazykarlcodes.dev/posts/create-a-portfolio/ Thu, 30 Jul 2020 19:28:30 -0700[email protected] (Karl Cardenas) https://crazykarlcodes.dev/posts/create-a-portfolio/ Posts <img src="./images/create-a-portfolio/resume.webp" alt="featured image" referrerpolicy="no-referrer"><p>Chances are most of us have stumbled into someone’s digital portfolio. This can range from book authors to IT professionals, it’s pretty universal as everyone can benefit from it. Is it needed, probably not but it does help quite a bit in showcasing your skills and who knows, perhaps it could be the difference between you getting an interview and/or a phone call. Let’s say you decided you want a digital portfolio, you’re probably wondering how to go about getting started? Does it cost money? Is it affordable? Do I have to be a programmer? Do I need to maintain a server? What if I told you can do it all for free, with no programming skills, and virtually zero maintenance. All you need is some patience, a little bit of guidance and some spare time. Sounds pretty neat right? Well if that interest you let’s dive into what it takes to make a digital portfolio.</p> <p><a href="https://medium.com/swlh/digital-portfolio-free-and-serverless-7a895b6232f1"target="_blank" rel="external nofollow noopener noreferrer">Medium Link<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></p> <p><strong>Note</strong>: If you get lost or want to look at the final example code, check out the <a href="https://gitlab.com/cardenas88karl/my-portfolio-example"target="_blank" rel="external nofollow noopener noreferrer">code respository<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></p> <h2 class="heading-element" id="getting-started"><span>Getting Started</span> <a href="#getting-started" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>In order to make this happen we will use the following pieces of technology.</p> <ul> <li>A domain name (optional)</li> <li>Your favorite static site framework (we will use Hugo)</li> <li><a href="https://www.netlify.com/"target="_blank" rel="external nofollow noopener noreferrer">Netlify<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></li> <li><a href="https://github.com/"target="_blank" rel="external nofollow noopener noreferrer">Github<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> or <a href="https://about.gitlab.com/"target="_blank" rel="external nofollow noopener noreferrer">Gitlab<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a></li> <li>A text editor It helps it if you are familiar with git beforehand and have git configured. <a href="https://kbroman.org/github_tutorial/pages/first_time.html"target="_blank" rel="external nofollow noopener noreferrer">Here<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> is a guide for setting up git with Github, and <a href="https://docs.gitlab.com/ee/topics/git/how_to_install_git/"target="_blank" rel="external nofollow noopener noreferrer">here<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> is one for Gitlab. There are also numerous videos on Youtube that walks you through how to get started if you are a visual learner.</li> </ul> <p>Next we need to make a decision, do we want a domain name, such as <em>example.com</em> or do want to leverage a generic name that might not be as human friendly? I recommend getting a domain name, plus domain names are not too expensive, about $10–12/yr. You can sacrifice two trips to Starbucks this week, and you’ll already have it paid off ☕️</p> <p>If you want help in acquiring a domain name read the next section, otherwise skip to the “Install Hugo” section.</p> <p><em>PS: If you already have a domain, ensure to change the preferred name servers (NS) to Netlify’s name servers. This is demonstrated in the next section.</em></p> <h2 class="heading-element" id="getting-a-domain-name"><span>Getting a Domain Name</span> <a href="#getting-a-domain-name" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>What’s awesome these days is that you can buy a domain name from various providers, and better yet, delegate the management of the domain name to another provider. If you haven’t already, log into Netlify and/or create an account (free). Next, click on domains.</p> <figure><img src="./images/create-a-portfolio/domain-0.webp"> </figure> <p>This will take you to a landing page. In this landing page there is a search bar, type the domain name you would like to acquire or transfer if you already own one. After you have come up with a domain name, type it in and Netlify will verify if it already exists. If the domain name is available you will be presented with the option to purchase. Keep in mind that you have to add a payment method before you can purchase the domain.</p> <figure><img src="./images/create-a-portfolio/domain-1.webp"> </figure> <p>Go ahead and purchase the domain and take the default settings in the following screens. We will revisit the domains page later for HTTPS.</p> <p>For those of you that already have a domain and want to transfer it, follow the same process and update the name servers. Below is an example of my personal domain that I purchased through Goolge domains. As you can see below, Netlify has four different name servers. These values need to be added in Google domains, or wherever your domain is registered.</p> <figure><img src="./images/create-a-portfolio/domain-3.webp"><figcaption> <h4>*Example of a domain registered through an external provider*</h4> </figcaption> </figure> <figure><img src="./images/create-a-portfolio/domain-4.webp"><figcaption> <h4>*Example of the name servers being updated in the external provider*</h4> </figcaption> </figure> <p>Once you have completed this step it might take few minutes to update. I’ve seen it take as short as 30 seconds to 10 minutes. You can verify this through <a href="https://toolbox.googleapps.com/apps/dig/#NS/"target="_blank" rel="external nofollow noopener noreferrer">G Suite toolbox<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> , if everything was done correctly you should see the Netlify name servers in the results field.</p> <figure><img src="./images/create-a-portfolio/domain-5.webp"> </figure> <h2 class="heading-element" id="install-hugo"><span>Install Hugo</span> <a href="#install-hugo" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>At this point we are ready to start playing with Hugo. If you’re not familiar with Hugo, don’t sweat it, just know that it’s a static site framework that does a lot of heavy lifting for us.</p> <p>To install Hugo, visit <a href="https://gohugo.io/getting-started/installing/"target="_blank" rel="external nofollow noopener noreferrer">https://gohugo.io/getting-started/installing/<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> . Installing Hugo can be a bit overwhelming if you don’t come from a technical background, start by reading <a href="https://gohugo.io/getting-started/installing/#less-technical-users"target="_blank" rel="external nofollow noopener noreferrer">this<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> section of the installation guide. There is also a pretty handy install YouTube video if you are on a <a href="https://www.youtube.com/watch?v=G7umPCU-8xc"target="_blank" rel="external nofollow noopener noreferrer">Windows<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>, or a <a href="https://www.youtube.com/watch?time_continue=1&amp;v=WvhCGlLcrF8&amp;feature=emb_logo"target="_blank" rel="external nofollow noopener noreferrer">Mac<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>.</p> <p>Open up command prompt, gitbash, or any terminal tool of your preference. You can verify Hugo installed correctly if the version command responds back with version information.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo version </span></span><span class="line"><span class="cl">Hugo Static Site Generator v0.74.2-48565DE6 linux/amd64 BuildDate: 2020-07-17T17:25:56Z</span></span></code></pre></td></tr></table> </div> </div><p>With Hugo installed, let’s go ahead and make a Hugo project. Go to a location on your system where you want the Hugo project stored. This part is important as the command we are about to issue will create a folder structure in the current directory.</p> <p><strong>The site command requires a name parameter as it will create a folder with that name</strong></p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">hugo new site new my-portfolio-example</span></span></code></pre></td></tr></table> </div> </div><p>We now have a folder titled <code>my-portfolio-example</code>. The next step is to get everything ready for git.</p> <h2 class="heading-element" id="create-a-git-repository"><span>Create a git repository</span> <a href="#create-a-git-repository" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>Assuming you have git installed, let’s go ahead change the directory to the Hugo project. If you’re on Windows you can use <a href="https://gitforwindows.org/"target="_blank" rel="external nofollow noopener noreferrer">gitbash<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>, command prompt, or <a href="https://docs.microsoft.com/en-us/windows/wsl/install-win10"target="_blank" rel="external nofollow noopener noreferrer">WSL<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. If you have not configured git yet go ahead and add your user name and email.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git config --global user.name <span class="s2">&#34;John Doe&#34;</span> </span></span><span class="line"><span class="cl">git config --global user.email <span class="s2">&#34;[email protected]&#34;</span></span></span></code></pre></td></tr></table> </div> </div><p>Let’s go ahead and create a repository in <a href="https://gitlab.com/"target="_blank" rel="external nofollow noopener noreferrer">Gitlab.com<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. If you have not created a Gitlab account, go ahead and do so now (free). If you prefer Github, that’s fine too, either one works. The end goal is to create a git project.</p> <figure><img src="./images/create-a-portfolio/gitlab-1.webp"><figcaption> <h4>Click on New Project</h4> </figcaption> </figure> <p>Once you click on “New Project” it will take us to a screen where we will be asked to provide a project name and a description followed by visibility settings. Go ahead and set your visibility to Private. Click on “Create project” once you have your name and description finalized.</p> <figure><img src="./images/create-a-portfolio/gitlab-2.webp"> </figure> <p>The next screen is the home of your project. Because there is no content in this project yet, we are presented with some commands to help us get started. We will use the “Push an existing folder” set of commands.</p> <p><strong>Note</strong>: You will be prompted for username and password if you have not configured Gitlab with an SSH key. <a href="https://docs.gitlab.com/ee/gitlab-basics/create-your-ssh-keys.html"target="_blank" rel="external nofollow noopener noreferrer">Here<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> is a job aid for configuring SSH for Gitlab.com</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span><span class="lnt">6 </span><span class="lnt">7 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="c1"># Example of the commands and how they apply to my project</span> </span></span><span class="line"><span class="cl"><span class="nb">cd</span> my-portfolio-example </span></span><span class="line"><span class="cl">git init </span></span><span class="line"><span class="cl">git remote add origin [email protected]/cardenas88karl/my-portfolio-example </span></span><span class="line"><span class="cl">git add -A </span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;Initial commit&#34;</span> </span></span><span class="line"><span class="cl">git push -u origin master</span></span></code></pre></td></tr></table> </div> </div><figure><img src="./images/create-a-portfolio/gitlab-3.webp"><figcaption> <h4>Example of the what it looks like after the content is uploaded</h4> </figcaption> </figure> <p>The next part is selecting a Hugo theme, Hugo has a strong community with creative individuals that have created copious publicly available themes for all of us to consume and customize to our liking. Oh and majority of them are free 🆓</p> <h2 class="heading-element" id="pick-a-website-theme"><span>Pick a website theme</span> <a href="#pick-a-website-theme" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>This is where the fun begins, picking a theme for your website. Let’s pick a theme for the portfolio website. Visit <a href="https://themes.gohugo.io/"target="_blank" rel="external nofollow noopener noreferrer">https://themes.gohugo.io/<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>.</p> <p>I really like the <code>hello-friend-ng theme</code>, so I’m gonna go with that one. Go into the theme and click on “Download”, this will take us to its Github location, in other words, where the source code resides. Theme creators will normally populate the README file with lots of valuable information, <strong>this is important when it comes to customizing your website and will be frequently referenced</strong>.</p> <p>If we go through the README, the author articulates how to consume the theme, in fact the author gives us two options. To either direct copy of the theme folder into our project OR to consume it as a git sub-module. There are pros and cons to both methods but for ease of use, let’s go with the git sub-module option. Issue the following command to add the theme into our existing Hugo project.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git submodule add https://github.com/rhazdon/hugo-theme-hello-friend-ng.git themes/hello-friend-ng</span></span></code></pre></td></tr></table> </div> </div><p>Now that we have the theme available, let’s change the <code>config.toml</code> and update it so that it picks up the new theme. The easiest way for you get started and start to understand how Hugo works is by replacing the <code>content</code> folder and <code>resources</code> folder with the ones found inside <code>theme/hello-freind-ng/exampleSite</code>. Below are the copy steps.</p> <ol> <li>Replace the <code>content/ </code>folder with the folder <code>theme/hello-friend-ng/exampleSite/content</code></li> <li>Replace the <code>resources/</code> folder with the folder <code>theme/hello-friend-ng/exampleSite/resources</code></li> <li>Replace the content of <code>config.toml</code> with what is in the config file found in <code>theme/hello-firend-ng/</code>. Inside the <code>config.toml</code> set the<code> baseUrl = &quot;&quot;</code> .</li> </ol> <p>Once you have this done let’s issue <code>hugo server -D</code> and open up our web browser and type in localhost:1313</p> <p><strong>Tip</strong>: <em>Visit the example repo if you got lost here to see the end result</em></p> <figure><img src="./images/create-a-portfolio/hugo-1.webp"><figcaption> <h4>What you should be seeing upon completing all the steps thus far.</h4> </figcaption> </figure> <p>Congratulations if you have made this far! I’m sure you’re wondering how to add content, pictures etc. So let’s dive into the Adding Content section.</p> <h2 class="heading-element" id="adding-content"><span>Adding Content</span> <a href="#adding-content" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>At this moment, the only content we have is from the original author of the theme. After all, we did copy the content folder from the exampleSite folder inside the theme. So let’s add a new page to the blog category. In order to do that we will leverage a Hugo command but first let’s stop the server <code>Ctrl + C</code> and open up your code editor.</p> <p>Let’s issue the command <code>hugo new content/posts/new-car.md</code>. This command creates a new file named <code>new-car.md</code> inside the <code>content/posts/</code> folder. We are creating the new file in this location because that’s where all blog post articles reside. Also, take note of the file extension <code>.md</code>, this is <a href="https://www.markdownguide.org/basic-syntax/"target="_blank" rel="external nofollow noopener noreferrer">markdown<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. If you’re not familiar with markdown take comfort in knowing that it’s a syntax that is incredible simple to learn and use for designing documents.</p> <p>If you open up <code>new-car.md</code> in your text edition you will see that in comes pre-populated with some content</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span><span class="lnt">3 </span><span class="lnt">4 </span><span class="lnt">5 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">--- </span></span><span class="line"><span class="cl">title: &#34;New Car&#34; </span></span><span class="line"><span class="cl">date: 2020-07-18T15:20:47-07:00 </span></span><span class="line"><span class="cl">draft: true </span></span><span class="line"><span class="cl">---</span></span></code></pre></td></tr></table> </div> </div><p>This content is derived from the <a href="https://gohugo.io/content-management/archetypes/"target="_blank" rel="external nofollow noopener noreferrer">archetype<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> dictated in the theme. Archetypes are pre-configured <a href="https://gohugo.io/content-management/front-matter/"target="_blank" rel="external nofollow noopener noreferrer">front matter<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. <strong>The important take away here is that the title is defined here, and that the draft setting is set to true. The draft setting should be set to false once the content is finalized. Otherwise, it will not be generated during the build process.</strong></p> <p>Let’s add the following content:</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt"> 1 </span><span class="lnt"> 2 </span><span class="lnt"> 3 </span><span class="lnt"> 4 </span><span class="lnt"> 5 </span><span class="lnt"> 6 </span><span class="lnt"> 7 </span><span class="lnt"> 8 </span><span class="lnt"> 9 </span><span class="lnt">10 </span><span class="lnt">11 </span><span class="lnt">12 </span><span class="lnt">13 </span><span class="lnt">14 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-md" data-lang="md"><span class="line"><span class="cl">--- </span></span><span class="line"><span class="cl">title: &#34;New Car&#34; </span></span><span class="line"><span class="cl">date: 2020-07-18T15:20:47-07:00 </span></span><span class="line"><span class="cl">draft: false </span></span><span class="line"><span class="cl">--- </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="gh"># A car for everyone </span></span></span><span class="line"><span class="cl"><span class="gh"></span> </span></span><span class="line"><span class="cl">Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry&#39;s standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl"><span class="gu">### How to buy a Car </span></span></span><span class="line"><span class="cl"><span class="gu"></span>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam nisi nulla, hendrerit a massa sed, blandit aliquam enim. Suspendisse placerat convallis augue eget convallis. Pellentesque ultrices cursus pretium. Donec aliquet auctor sodales. Donec vel finibus libero, ornare commodo leo. Nullam volutpat vel mauris sit amet aliquet. Etiam ultrices arcu nec dignissim maximus. Vestibulum accumsan tellus sit amet porta mattis. Proin egestas vel dui a egestas. Phasellus semper erat orci, et feugiat dui mollis vitae. </span></span><span class="line"><span class="cl"> </span></span><span class="line"><span class="cl">Suspendisse auctor velit tempor felis tincidunt, at semper lacus ultrices. Vivamus ac bibendum velit, eu aliquam mauris. In bibendum, lacus id congue elementum, tortor lectus tempor massa, quis semper enim mauris facilisis elit. Maecenas a nibh maximus, tristique urna nec, venenatis ipsum. Morbi lacus tortor, sagittis vitae dapibus eget, tempus non tortor. Phasellus eros lorem, sagittis sit amet commodo vitae, lacinia sit amet odio. Aliquam erat volutpat. Maecenas tincidunt justo nec ex elementum dictum. Nulla elementum massa tortor, vel luctus lorem condimentum sed. Phasellus et tincidunt diam, at sollicitudin nisi. Fusce quis urna nisl. Curabitur ultrices risus nec hendrerit faucibus. Donec luctus eget ipsum vel rutrum. Mauris non odio enim. Mauris aliquet feugiat metus, quis auctor mi maximus id.</span></span></code></pre></td></tr></table> </div> </div><p>All we are adding is a main heading and some random Latin text (<a href="https://www.lipsum.com/"target="_blank" rel="external nofollow noopener noreferrer">Lorem Ipsum<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>) and a link to a YouTube video using Hugo’s build in <a href="https://gohugo.io/content-management/shortcodes/"target="_blank" rel="external nofollow noopener noreferrer">short-code<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> for Youtube videos. Hugo has some pretty neat build in capabilities called short-code. I recommend becoming familiar with these to make your life easier.</p> <p>Let’s change the the draft setting to <code>false</code> (at the top of the newly created page) and boot up our server <code>hugo server -D</code> . You should now be able to view your new content by navigating to the <code>posts</code> tab and click on the new entry titled “New Car”.</p> <figure><img src="./images/create-a-portfolio/hugo-content.webp"><figcaption> <h4>What your newly added content should look like!</h4> </figcaption> </figure> <p>Let’s assume at this point that you are satisfied with the content and have everything you want out of your theme. Let’s push our content up to our Gitlab repository.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add -A <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;adding last bit of content before deploment&#34;</span> <span class="o">&amp;&amp;</span> git push origin master</span></span></code></pre></td></tr></table> </div> </div><p>And on that note, let’s jump into Netlify to deploy our application!</p> <p><em>For demonstration purposes we will not do any customization but the expectation is that you will customize the theme and content for your individual project. Always visit the theme’s Github repo to learn how to customize the theme.</em></p> <h2 class="heading-element" id="deploy-your-application"><span>Deploy your application</span> <a href="#deploy-your-application" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>In Netlify, go ahead and log in and click on “New Site from Git”</p> <p>After clicking on “New Site” you will be taken to a screen where you will select the Git provider for your code. If you have followed along and used Gitlab then go ahead and select Gitlab. You will be asked to give Gitlab access to your account and will have to log in. Once that is done you will be presented with a list of all your projects. Go ahead and select your project name, in this example my project is called <code>my-portfolio-example</code>.</p> <p>The next page allows you pick the git branch and and other related settings to our build process. Go ahead and stick with the defaults and click on “Deploy Site”.</p> <figure><img src="./images/create-a-portfolio/deploy-3.webp"><figcaption> <h4>Taking the defaults for Hugo</h4> </figcaption> </figure> <p>Clicking on the deploy site will launch the build and deploy process. This is one the main benefits of Netlify. The build and deployment process is automated, taking a lot of the technical heavy lifting of your back. The end result will take you you back to the main landing page but this time you will have a link to your new website. Go ahead and click on the link and visit your first portfolio!</p> <figure><img src="./images/create-a-portfolio/deploy-4.webp"><figcaption> <h4>Back to main site but with a link to our site!</h4> </figcaption> </figure> <figure><img src="./images/create-a-portfolio/deploy-5.webp"><figcaption> <h4>Working site hosted in Netlify</h4> </figcaption> </figure> <p><strong>Important</strong>: If the build fails it could be due to the Hugo version Netlify auto selected. Create a <code>netlify.toml</code> and specify the desired Hugo version. Ideally, you would specify the same version that you have installed locally. You can find the latest Hugo release <a href="https://github.com/gohugoio/hugo/releases"target="_blank" rel="external nofollow noopener noreferrer">here<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>. In the example <a href="https://gitlab.com/cardenas88karl/karl-cardenas-coding-blog"target="_blank" rel="external nofollow noopener noreferrer">repo<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> you can find an example of a <code>netlify.toml</code> configuration.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span><span class="lnt">2 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="p">[</span><span class="l">context.production.environment]</span><span class="w"> </span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="l">HUGO_VERSION = &#34;0.74.2&#34;</span></span></span></code></pre></td></tr></table> </div> </div><h2 class="heading-element" id="adding-a-custom-domain-name"><span>Adding a custom domain name</span> <a href="#adding-a-custom-domain-name" class="heading-mark"> <svg class="octicon octicon-link" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg> </a> </h2><p>As you probably have realized by now is that the url name generated by Netlify is not very user friendly. It probably has some random funny names followed by odd numbers and characters, like this one <a href="https://romantic-brown-3b847a.netlify.app/"target="_blank" rel="external nofollow noopener noreferrer">https://romantic-brown-3b847a.netlify.app/<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> 😝</p> <p>This is where a custom domain name comes into play, if you don’t want the auto-generated url then we need to provide a custom domain. If you bought a custom domain earlier and followed the registration steps for domain management in Netlify then keep on reading, otherwise I encourage you to re-visit the “Getting a Domain Name” section earlier in the article.</p> <p>In the main Netlify page, click on “Domain Settings”. If you added a custom domain earlier then make sure that the the domain(s) are listed and available. In the example below, you can see that my domain name <code>crazykarlcodes.dev</code> is listed and available.</p> <figure><img src="./images/create-a-portfolio/deploy-7.webp"><figcaption> <h4>Example domain names</h4> </figcaption> </figure> <p>In the file <code>config.toml</code> make sure the the baseUrl setting is pointing to your desired domain name. Example: <code>baseURL = &quot;https://crazykarlcodes.dev/&quot;</code> This is an important step because otherwise you might encounter come content failing to load due to <code>invalid CORS settings</code>. If you need to make the change, simply go back to the code editor and open up the config.toml file. Push your changes up to Gitlab when you are done.</p> <div class="highlight"><div class="chroma"> <table class="lntable"><tr><td class="lntd"> <pre tabindex="0" class="chroma"><code><span class="lnt">1 </span></code></pre></td> <td class="lntd"> <pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git add -A <span class="o">&amp;&amp;</span> git commit -m <span class="s2">&#34;updated baseURL setting in config.toml&#34;</span> <span class="o">&amp;&amp;</span> git push origin master</span></span></code></pre></td></tr></table> </div> </div><p>The last step is to configure the website for HTTPS traffic. This is to make sure the content is encrypted, otherwise most modern web browser will give the user a scary warning advising them to avoid the website due to lacking encryption. This is a good thing in general but bad for our website so let’s make sure we avoid this situation.</p> <p>In the same settings page for adding a custom domain, scroll down to the bottom. You should see a heading named “HTTPS”. Click on the button to add a TLS certificate. This will use DNS validation to make sure you own the domain. This is normally a pretty quick process but can sometimes take some time ( &gt; 30 min ). Once the certificate is created, go ahead and visit your website using the custom domain. Another great feature of Netlify is that this process is automated and free. This is normally a highly technical process and often cost money. You can thank Netlify and Let’sEncrypt for making this a free feature.</p> <p>If you encounter some issues checkout Netlify’s debugging <a href="https://docs.netlify.com/domains-https/troubleshooting-tips/#dns-configuration"target="_blank" rel="external nofollow noopener noreferrer">site<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a>.</p> <figure><img src="./images/create-a-portfolio/deploy-8.webp"> </figure> <hr> <p>This is only the beginning, at this point you have all the tools available to start customizing the content, look and style of your digital portfolio. Go back and visit the documentation for your selected theme and start learning the many amazing features that Hugo provides out of the box. Go look at other themes and example codes to see how others are using Hugo.</p> <p>If your theme does not have a feature or perhaps does not meets your requirements then checkout other themes. It’s also possible you might have to customize an existing theme or create your very own Hugo theme. Creating your own Hugo theme is certainly a bit of an advanced topic but if you were able to get to this point then I have no doubt you have it takes to learn Hugo and create your own theme.</p> <p>I hope you found this useful and wish for you new digital portfolio to bring great new opportunities for you! Feel free to checkout my <a href="https://crazykarlcodes.dev/"target="_blank" rel="external nofollow noopener noreferrer">digital portfolio<i class="fa-solid fa-external-link-alt fa-fw fa-xs ms-1 text-secondary" aria-hidden="true"></i></a> for inspiration, it may look familiar to what we worked on 😉</p>