<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Brian Caffey</title>
    <description>The latest articles on DEV Community by Brian Caffey (@briancaffey).</description>
    <link>https://dev.to/briancaffey</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F108648%2F62236586-18b9-4888-ac9e-16d7db32a96e.png</url>
      <title>DEV Community: Brian Caffey</title>
      <link>https://dev.to/briancaffey</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/briancaffey"/>
    <language>en</language>
    <item>
      <title>RedLM: My submission for the NVIDIA and LlamaIndex Developer Contest</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Thu, 14 Nov 2024 09:59:15 +0000</pubDate>
      <link>https://dev.to/briancaffey/redlm-my-submission-for-the-nvidia-and-llamaindex-developer-contest-1c3k</link>
      <guid>https://dev.to/briancaffey/redlm-my-submission-for-the-nvidia-and-llamaindex-developer-contest-1c3k</guid>
      <description>&lt;h2&gt;
  
  
  td;dr
&lt;/h2&gt;

&lt;p&gt;RedLM is a new way to study art and literature powered by artificial intelligence. It is an application that applies LLMs to the study of one of China’s most famous literary works: Dream of the Red Chamber. It uses leading language and vision models from Chinese AI groups including Alibaba’s Qwen, Baichuan Intelligence Technology and 01.AI. RedLM uses tools, techniques and services from NVIDIA and LlamaIndex including NVIDIA NIMs, Retrieval Augmented Generation and Multi-Modal RAG with vision language models. This project is my submission for the NVIDIA and LlamaIndex Developer Contest.&lt;/p&gt;

&lt;p&gt;This article will cover how I built the project, challenges I faced and some of the lessons I learned while working with NVIDIA and LlamaIndex technologies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Links
&lt;/h3&gt;



&lt;ul&gt;
&lt;li&gt;&lt;a href="https://x.com/briancaffey/status/1855186768452321330" rel="noopener noreferrer"&gt;𝕏 Post&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/briancaffey/RedLM" rel="noopener noreferrer"&gt;RedLM GitHub repository&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What is RedLM?
&lt;/h2&gt;

&lt;p&gt;RedLM is a combination of the word “Red” and LM, an abbreviation for “language model”. Dream of the Red Chamber is such an important book in Chinese literature that it has its own field of study called 红学 (literally “the study of red”), or Redology. So, RedLM is an application that uses language models for the study of Redology.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fje1sys0v8eidewvbfxr0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fje1sys0v8eidewvbfxr0.png" alt="RedLM" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this project I focused on three applications of language models:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Summary and translation of the source text&lt;/li&gt;
&lt;li&gt;A Q&amp;amp;A bot that can answer questions about the book providing references to the specific paragraphs used to give answers&lt;/li&gt;
&lt;li&gt;An image-based Q&amp;amp;A bot that can answer questions about sections of paintings that depict scenes from each of the book’s chapters.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  NotebookLM
&lt;/h2&gt;

&lt;p&gt;I used this article to create a "Deep Dive" podcast episode for RedLM using Google's NotebookLM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fja5vpp9ckm4hha6u7dbr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fja5vpp9ckm4hha6u7dbr.png" alt="NotebookLM" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://x.com/briancaffey/status/1855186771409244491" rel="noopener noreferrer"&gt;listen to this podcast episode here on 𝕏&lt;/a&gt;.&lt;/p&gt;



&lt;h2&gt;
  
  
  How I built RedLM
&lt;/h2&gt;

&lt;p&gt;RedLM consists of two parts: a web UI built with Vue 3 using the Nuxt Framework and a backend API built with Python, FastAPI and LlamaIndex. There are lots of great tools for building full-stack AI applications such as Gradio and Streamlit, but I wanted to build with the web tools that I’m most familiar with and that provide the most flexibility. These frameworks (Nuxt and FastAPI) are simple and effective and they allowed me to develop quickly.&lt;/p&gt;

&lt;p&gt;Most of the code for this project was written by AI. I used OpenAI’s ChatGPT 4o, Anthropic’s Claude 3.5 Sonnet and 01.AI’s Yi-1.5-9B-Chat model. In my development process with AI, I prompted for one logical piece of the application at a time, such as one API route, one Vue component, one pinia store or one utility function, for example. In this article I'll share some of the prompts I used in my development workflow.&lt;/p&gt;

&lt;p&gt;This project embraces a hybrid AI inference model, meaning that the AI inference can be done either on local RTX PCs or using NVIDIA’s Cloud APIs from &lt;code&gt;build.nvidia.com&lt;/code&gt; depending on configuration via environment variables. I used PCs with NVIDIA GeForce RTX 4090 GPUs to do inference with language and vision models, and with a change of configuration, I was able to do similar inference using NVIDIA’s API endpoints. This allowed me to develop the project both on powerful RTX desktop workstations and Mac laptops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Translating Dream of the Red Chamber with TensorRT-LLM
&lt;/h2&gt;

&lt;p&gt;Translation is often mentioned as one of the capabilities of bilingual LLMs from China. I wanted to try translating this book from Chinese to English, but I also wanted to better understand the meaning of the original text written in vernacular Chinese. Written vernacular Chinese is essentially a form of Chinese that closely resembles the way Chinese was spoken in imperial China by common people. The use of vernacular Chinese (Baihua) in literary works marked a significant cultural shift that started to make literature and education more accessible. Before the emergence of written vernacular Chinese, Chinese literature was dominated by Classical Chinese (Wenyanwen) which is a more concise, ambiguous and specialized for of languages that assumes an understanding of ancient texts and Confucian classics. The difference between vernacular Chinese and modern Mandarin Chinese is somewhat analogous to the different between Shakespearian English (Early Modern English) and Modern English.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0aibg6i9pgylunfwfhg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0aibg6i9pgylunfwfhg.png" alt="Baihua, Mandarin and English" width="800" height="312"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chinese large language models are well versed in Classical Chinese, written Chinese vernacular and modern Mandarin Chinese. I decided to rewrite the original vernacular text in simple, modern Mandarin Chinese and then using this new modern Mandarin version, translate the story into English.&lt;/p&gt;

&lt;p&gt;Dream of the Red Chamber is a large book. It is composed of over 800,000 Chinese characters, using 4303 unique Chinese characters. It has 120 chapters and a total of 3996 paragraphs. Here is a histogram showing the number of characters per paragraph.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr94k3j2wheprimp401in.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr94k3j2wheprimp401in.png" alt="Paragraph lengths" width="800" height="369"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I rented a large multi-GPU instance from AWS using some of the credits I get as a member of the AWS Community Builders program. The g5.12xlarge instance I selected has 4 A10G Tensor Core GPUs. The TensorRT-LLM LLM API is a relatively new part of the TensorRT-LLM library. It provides a very simple, high-level interface for doing inference. Following the &lt;a href="https://nvidia.github.io/TensorRT-LLM/llm-api-examples/llm_inference_distributed.html" rel="noopener noreferrer"&gt;LLM Generate Distributed example&lt;/a&gt; from the TensorRT-LLM documentation, I was able to translate the entire book into simple Mandarin and then from Mandarin into English in about an hour and 15 minutes. The &lt;code&gt;tensor_parallel_size&lt;/code&gt; option in the LLM API allows for distributed inference, this meant that up to 4 paragraphs could be translated at the same time on different GPUs on the same EC2 instance.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Translating: data/book/22.json
Processed requests: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 38/38 [00:15&amp;lt;00:00,  2.41it/s]
Processed requests: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 38/38 [00:24&amp;lt;00:00,  1.54it/s]
Translated: data/book/22.json
Translating: data/book/114.json
Processed requests: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 20/20 [00:11&amp;lt;00:00,  1.81it/s]
Processed requests: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 20/20 [00:12&amp;lt;00:00,  1.58it/s]
Translated: data/book/114.json
[TensorRT-LLM][INFO] Refreshed the MPI local session
[TensorRT-LLM][INFO] Refreshed the MPI local session
[TensorRT-LLM][INFO] Refreshed the MPI local session
[TensorRT-LLM][INFO] Refreshed the MPI local session

real    74m1.578s
user    0m45.936s
sys 0m36.283s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getting good results required a bit of experimentation with parameters. The LLM API makes this very easy. The following code configures settings and builds the inference engine that can be used for doing completions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;sampling_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SamplingParams&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;top_p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;build_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BuildConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_seq_len&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;LLM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;MODEL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;build_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;build_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tensor_parallel_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used the following prompts to rewrite each paragraph of the original text in simple, modern Mandarin Chinese:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;bai_prompts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;# Here are examples of how to rewrite Chinese vernacular into simple modern Mandarin.\n\nChinese vernacular:\n\n{p}\n\nSimple modern Mandarin
&lt;/span&gt;    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;以下是如何将中国白话改写为简单的现代普通话的示例。&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;中文白话：&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;简单的现代普通话：&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flat_bai&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was difficult to get good results consistently. Here are some observations I had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some of the translated paragraphs were perfect&lt;/li&gt;
&lt;li&gt;some translated paragraphs would randomly hallucinate the same phrase over and over again&lt;/li&gt;
&lt;li&gt;Some requests to translate text to English would reply in Mandarin Chinese rather than in English&lt;/li&gt;
&lt;li&gt;Sometimes I would even see computer code generated when asking for a translation&lt;/li&gt;
&lt;li&gt;The names of characters were sometimes translated inconsistently, sometimes literally and sometimes using differing versions of pinyin, the Romanization system for transcribing the sounds of Mandarin Chinese&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I found that ChatGPT 4o could handle any Chinese translation task flawlessly, but the &lt;code&gt;Qwen2-7B&lt;/code&gt; model I used had mixed results! The change that I made that seemed to have the biggest impact on translation quality was setting &lt;code&gt;*max_tokens*=256&lt;/code&gt; in &lt;code&gt;SamplingParams&lt;/code&gt;. I probably could have used a dynamic value for &lt;code&gt;max_tokens&lt;/code&gt; based on the size of the current paragraph being translated. I also would have like to set up side-by-side comparisons of translations using different sized models, but rather than spend time and AWS credits on optimizing translation with TensorRT-LLM, I wanted to focus on the main part of this project: retrieval augmented generation (RAG) with LlamaIndex.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building Q&amp;amp;A bots with RAG using LlamaIndex
&lt;/h2&gt;

&lt;p&gt;My primary objective with this project was to implement a simple chat bot that responds to questions about the book with references to the specific paragraphs used in the response. The following shows images of the UI I built with one of the examples I included in the video I made for this project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5gdaqrga1yw9bt4pu7kv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5gdaqrga1yw9bt4pu7kv.png" alt="RAG Example" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I haven't read that much of the book before working on this project, but I have read a lot &lt;em&gt;about&lt;/em&gt; this book's characters, major themes and plot. This Q&amp;amp;A bot was a very interesting entrypoint to explore specific passages of the book starting with questions coming from my knowledge about the book. The question in the screenshots above is: “What does Jia Baoyu’s father think about him?” The response includes references to paragraphs where Jia Zheng (Baoyu’s father) is discussing his son. I was pretty amazed that the RAG query was able to pull out these two paragraphs.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;In Dream of the Red Chamber, the relationship between protagonist Jia Baoyu and his father, Jia Zheng, is complex and fraught with tension. Jia Zheng, a strict, traditional Confucian patriarch, embodies values of discipline, scholarly rigor, and duty. He expects his son to excel in his studies and uphold the family’s honor by pursuing an official career in government. Baoyu, however, is sensitive, imaginative, and inclined toward poetry and the company of women, especially his cousins Lin Daiyu and Xue Baochai. This preference clashes with Jia Zheng’s expectations, leading to frequent misunderstandings and disappointment.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;By default, LlamaIndex uses cosine similarity as the distance metric for finding the vectors representing the documents (paragraphs) that are “closest” to the vector representing the user query. This is the central mechanism by which RAG works. LlamaIndex provides an abstraction of this process, hiding the implementation details and allowing rapid development of retrieval systems.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F45v8gj3uav8vx1ufw5ad.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F45v8gj3uav8vx1ufw5ad.png" alt="Cosine Similarity" width="800" height="426"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Source: &lt;a href="https://medium.com/@kbdhunga/a-beginners-guide-to-similarity-search-vector-indexing-part-one-9cf5e9171976" rel="noopener noreferrer"&gt;https://medium.com/@kbdhunga/a-beginners-guide-to-similarity-search-vector-indexing-part-one-9cf5e9171976&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here is some of the code I wrote for the text-based Q&amp;amp;A bot using LlamaIndex’s &lt;code&gt;CustomQueryEngine&lt;/code&gt; class to fetch the nodes from which I get the referenced paragraph text, chapter number and paragraph number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;QAndAQueryEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CustomQueryEngine&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;RAG Completion Query Engine optimized for Q&amp;amp;A&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseRetriever&lt;/span&gt;
    &lt;span class="n"&gt;response_synthesizer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BaseSynthesizer&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;OpenAILike&lt;/span&gt;
    &lt;span class="n"&gt;qa_prompt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PromptTemplate&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;custom_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query_str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="c1"&gt;# Collect the metadata into a list of dicts so that it can be sent to UI for references
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;metadata_dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
            &lt;span class="n"&gt;node_metadata&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metadata&lt;/span&gt;
            &lt;span class="n"&gt;metadata_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
            &lt;span class="n"&gt;metadata_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chapter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node_metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chapter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;metadata_dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paragraph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node_metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paragraph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

            &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metadata_dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;context_str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_content&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="nc"&gt;ChatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;q_and_a_prompt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="c1"&gt;# the English and Chinese prompt templates are discussed below
&lt;/span&gt;                        &lt;span class="n"&gt;context_str&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;context_str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_str&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;query_str&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Indexing the book data
&lt;/h3&gt;

&lt;p&gt;In the indexing process, embedding models are used to translate chunks of text (paragraphs) into high-dimensional vectors that represent the relationships between the tokens in a chunk of text. These are the vectors stored in the "Vector Database" used by LlamaIndex. The chapter number, paragraph number and version (original, Mandarin Chinese and English) of each paragraph are added to the database entry as metadata during the indexing step which runs via a script before starting the FastAPI server. Here's how I indexed the original text and translations with LlamaIndex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llama_index.core&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VectorStoreIndex&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llama_index.embeddings.huggingface&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HuggingFaceEmbedding&lt;/span&gt;

&lt;span class="n"&gt;en_embedding_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HuggingFaceEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BAAI/bge-small-en-v1.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;zh_embedding_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HuggingFaceEmbedding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BAAI/bge-small-zh-v1.5&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;persist_index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chapter&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;121&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data/book/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;chapter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;paragraphs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paragraphs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paragraphs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;original&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chinese&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;english&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="n"&gt;document&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                    &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chapter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chapter&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paragraph&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                    &lt;span class="n"&gt;metadata_seperator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;::&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;metadata_template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{key}=&amp;gt;{value}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;text_template&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Metadata: {metadata_str}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;-----&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Content: {content}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;embed_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;en_embedding_model&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lang&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;english&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;zh_embedding_model&lt;/span&gt;
                    &lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;document&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VectorStoreIndex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storage_context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;persist&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;persist_dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;storage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;persist_index&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the embedding models, I used the small BAAI General Embedding models (BGE) for English and Chinese. BAAI is the Beijing Academy of Artificial Intelligence, and I learned about this organization through some of the examples on the LlamaIndex site that use BAAI embeddings. There are multi-lingual embedding models (e.g. &lt;code&gt;BAAI/bge-m3&lt;/code&gt;), but setting the embedding model on a per-document basis is possible and in some cases it might be preferable to using a single embedding model for all documents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Milvus Vector Database
&lt;/h3&gt;

&lt;p&gt;I did most of the development for this project using the in-memory VectorIndexStore provided by LlamaIndex. This worked well, but making any changes to the FastAPI server required the data to be reloaded into memory which took several seconds each time. This can really hinder a good development flow, so I looked into using an external service for the vector database instead of running it in memory.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fts3rsudmb2kywgp2lwyq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fts3rsudmb2kywgp2lwyq.png" alt="Vector Database Options" width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are a LOT of options to consider when picking a vector database for a RAG application. Milvus has a highly decoupled architecture, it is fully open source and I had seen it in some examples in the &lt;a href="https://github.com/NVIDIA/GenerativeAIExamples/tree/main/RAG/examples/advanced_rag/multimodal_rag" rel="noopener noreferrer"&gt;&lt;code&gt;NVIDIA/GenerativeAIExamples&lt;/code&gt;&lt;/a&gt; repo, so I decided to give it a try.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fksnz2qewjqkxwp22hr1f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fksnz2qewjqkxwp22hr1f.png" alt="Milvus Vector Database Architecture" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using the &lt;a href="https://milvus.io/docs/v2.0.x/install_standalone-docker.md" rel="noopener noreferrer"&gt;Milvus docker compose example&lt;/a&gt; I was able to set up an external vector database based on etcd and minio. Milvus also provides a Helm chart for running their vector database, this would be helpful if I was going to be running everything in Kubernetes (inference, vector database and application containers).&lt;/p&gt;

&lt;h3&gt;
  
  
  Other examples of RAG with English questions
&lt;/h3&gt;

&lt;p&gt;One interesting design question I faced was how to support answering questions in both English and Chinese. I initially built the Q&amp;amp;A bot with only Chinese language support. Later, I added a simple helper function to determine if the input text is Chinese:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_chinese_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    This is a simple helper function that is used to determine which prompt to use
    depending on the language of the original user query
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;chinese_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\u4e00&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\u9fff&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;english_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;char&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;z&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chinese_count&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;english_count&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This boolean value would then be used in the &lt;code&gt;CustomQueryEngine&lt;/code&gt; to use either the Chinese or English &lt;code&gt;PromptTemplate&lt;/code&gt;. This allowed the Q&amp;amp;A bot to answer questions in either Chinese or English, and it does not require translating back and forth between Chinese and English. However, this method relies on high-quality translations, so I don't expect English language questions to be answered as accurately as Chinese language questions. Here are the Chinese and English prompts that I used for the text-based Q&amp;amp;A bot, as well as some examples of the Q&amp;amp;A bot answering questions in English. The referenced materials include paragraphs from the English translation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Chinese prompt for text-based Q&amp;amp;A bot
&lt;/span&gt;&lt;span class="n"&gt;q_and_a_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PromptTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;这是相关的参考资料：&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---------------------&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{context_str}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# context_str contains Chinese paragraphs retrieved via RAG query
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---------------------&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;根据上述的参考资料，回答下面的问题&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;问题：{user_question}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# English prompt for text-based Q&amp;amp;A bot
&lt;/span&gt;&lt;span class="n"&gt;q_and_a_prompt_english&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PromptTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This is some related reference material:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---------------------&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{context_str}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="c1"&gt;# context_str contains English paragraphs retrieved via RAG query
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---------------------&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Based on the above material, answer the following question:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Question: {user_question}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fspwbe18sqzo5m8diyon2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fspwbe18sqzo5m8diyon2.png" alt="Multi-modal Q&amp;amp;A example 1" width="800" height="341"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Asking random questions like this one is a fun way to explore the many scenes of Dream of the Red Chamber.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpfa1lj87ww6jzz765h83.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpfa1lj87ww6jzz765h83.png" alt="RAG Flower Pedal Example" width="800" height="380"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff5p6w467j6g1u66ksorr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff5p6w467j6g1u66ksorr.png" alt="RAG Flower Pedal Example with Reference" width="800" height="626"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  RedLM RAG Evaluation
&lt;/h2&gt;

&lt;p&gt;Examinations have long been a cornerstone of Chinese society, shaping individual aspirations, cultural values, and even government structures. This legacy began with the imperial civil service exams, kējǔ (科举), established during the Sui and Tang dynasties, and carries through in Modern China with the gaokao (高考) college entrance examination, both of which have allowed for unprecedented meritocratic routes to power and prestige. Given how widely this novel is studied in China, I was not surprised to find a wealth of examination questions written for students studying Dream of the Red Chamber.&lt;/p&gt;

&lt;p&gt;I used &lt;a href="https://www.examcoo.com/editor/do/view/id/246401" rel="noopener noreferrer"&gt;a set of 1000 multiple choice questions about Dream of the Red Chamber on examcoo.com&lt;/a&gt; to evaluate the effectiveness of the RAG system I built with LlamaIndex. I wrote a script to parse the questions from the website HTML using ChatGPT (parsing HTML is one of my favorite use cases of LLMs!) I filtered the list of 1000 questions down to 877 questions based on the following criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Four answer choices&lt;/strong&gt;: some of the questions had more than four answer choices. I filtered questions with more than four answer choices to keep the evaluation simple. This would allow me to assume that random answer choices would have a 25% chance of being correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only one answer&lt;/strong&gt;: For some questions the correct answer required selecting multiple answer choices. This would also help keep the evaluation logic simple.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1t7jb8w4fqpqv7n9khe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fo1t7jb8w4fqpqv7n9khe.png" alt="Multiple Choice Questions from Dream of the Red Chamber Test" width="800" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Multiple choice questions from a Dream of the Red Chamber test (examcoo.com)&lt;/p&gt;

&lt;p&gt;To run the evaluation I set up two scripts. The first script would prompt the LLM to answer the question without any additional information from the RAG system. This served as a baseline to see how well the LLM could do at answering multiple choice questions about the book. The script simply checks to see if the LLM response contains the letter (A, B, C or D) of the correct answer and keeps track of the number of questions answered correctly.&lt;/p&gt;

&lt;p&gt;Another script was used to take the test using large language models with RAG. In this script, the prompt sent to the LLM included relevant paragraphs from the book based on how similar the query is to each paragraph in the book based on the cosine similarity metric mentioned earlier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikdrgon3xr1h1qe6i2i0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fikdrgon3xr1h1qe6i2i0.png" alt="RAG evaluation" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are some results and other observations from this experiment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLMs alone scored in the mid 30% range (36%)&lt;/li&gt;
&lt;li&gt;LLMs using retrieval augmented generation with the set of questions score in the mid 40% range (44%)&lt;/li&gt;
&lt;li&gt;I used the completion API rather than the chat API and set the &lt;code&gt;max_tokens&lt;/code&gt; to 16. This was done to ensure that the LLM only gave a short response with a valid answer choice rather than giving a long response with an explanation.&lt;/li&gt;
&lt;li&gt;The evaluation took longer for LLM + RAG test because of the time required for making the RAG query and the longer prompt (including both the original multiple-choice question and the referenced paragraphs).&lt;/li&gt;
&lt;li&gt;I used the &lt;code&gt;01-ai/Yi-1.5-9B-Chat&lt;/code&gt; model for this test, but I probably should have used the base model rather than the chat model.&lt;/li&gt;
&lt;li&gt;Some questions would not be capable of being answered by RAG. For example, some of the questions are about film renditions of the novel. Most of the questions seemed relevant to the content of the book, so I didn’t bother to filter out the questions that were not directly related to the book’s content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is an example of a question that the LLM test script answered &lt;em&gt;incorrectly&lt;/em&gt; and the LLM + RAG test script answered &lt;strong&gt;correctly&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;秦钟的父亲是如何死的？&lt;/p&gt;

&lt;p&gt;A、外感风寒、风毒之症&lt;/p&gt;

&lt;p&gt;B、被智能儿气死的&lt;/p&gt;

&lt;p&gt;C、生气引发旧病加重&lt;/p&gt;

&lt;p&gt;D、生气而诱发中风而死&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Translation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How did Qin Zhong's father die?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A.&lt;/strong&gt; He caught a cold and developed wind-related illnesses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;B.&lt;/strong&gt; He was angered to death by Zhineng'er (a character).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;C.&lt;/strong&gt; His old illness worsened due to anger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;D.&lt;/strong&gt; He had a stroke induced by anger and died.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is the paragraphs that the RAG query returned along with the English translation:&lt;/p&gt;

&lt;p&gt;Original&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;荣两处上下内外人等莫不欢天喜地，独有宝玉置若罔闻。你道什么缘故？原来近日水月庵的智能私逃入城，来找秦钟，不意被秦邦业知觉，将智能逐出，将秦钟打了一顿，自己气的老病发了，三五日便呜呼哀哉了。秦钟本自怯弱，又带病未痊，受了笞杖，今见老父气死，悔痛无及，又添了许多病症。因此，宝玉心中怅怅不乐。虽有元春晋封之事，那解得他的愁闷？贾母等如何谢恩，如何回家，亲友如何来庆贺，宁荣两府近日如何热闹，众人如何得意，独他一个皆视有如无，毫不介意。因此，众人嘲他越发呆了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;English&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Everyone in the Rong and Ning households, both inside and outside, were extremely happy, except for Baoyu, who seemed indifferent. Do you want to know why? It turns out that recently, the nun Zhineng from Shuiyue Temple secretly ran into the city to find Qin Zhong. Unexpectedly, she was discovered by Qin Zhong's father, Qin Banger. Qin Banger not only drove Zhineng away but also gave Qin Zhong a beating. This made Qin Banger so angry that his old illness relapsed, and within three to five days, he passed away. Qin Zhong had always been weak and hadn't fully recovered from a previous illness. After being beaten and seeing his father die in anger, he was overwhelmed with regret and sorrow, which worsened his condition. As a result, Baoyu felt very melancholic. Although the promotion of Yuan Chun to imperial concubine was a joyful event, it couldn't alleviate the gloom in his heart. While Grandmother Jia and others were busy expressing their gratitude and returning home, and relatives and friends came to celebrate, and the Rong and Ning households were bustling with excitement, Baoyu alone remained completely indifferent to it all. Consequently, everyone started to mock him for becoming more and more absent-minded.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The correct answer for this question is C.&lt;/p&gt;
&lt;h2&gt;
  
  
  Multi-modal RAG for visual reasoning
&lt;/h2&gt;

&lt;p&gt;Qwen2-VL is a new AI model that was released in late August 2024. Qwen is the name of Alibaba’s AI Lab, and it is an abbreviation of the Chinese characters: 千问 ("qian wen", meaning 1000 questions). VL stands for vision-language, meaning that the model is capable of understanding both text and images. I had tested out the previous version of Qwen’s vision-language model and was very impressed by how it could accurately describe the contents of images and also answer general questions about images.&lt;/p&gt;

&lt;p&gt;Sun Wen was a Qing-era painter who spent 36 years of his life creating a series of 230 paintings capturing scenes from Dream of the Red Chamber. The paintings are incredibly detailed and often contain repeated figures in a temporal sequence. If you asked a Qwen-VL model to describe one of the images, it might return lengthy description that doesn't fully capture the full detail of the scene. It might also be difficult for a language model to "focus" on a portion of the whole image.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkqgi9od47n74p9izz0zy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkqgi9od47n74p9izz0zy.png" alt="Dream of the Red Chamber Painting 131" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This sparked the idea to create a feature where users can click and drag over an image to select part of a painting, then ask questions specifically about the selected portion. I knew that while this could be achieved with tools like HTML canvas, writing it on my own would be quite time-consuming. It took me just a few minutes to write out the prompt, and Claude 3.5 Sonnet generated a perfect prototype of this feature in under a minute. Here’s the prompt I used:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm going to describe a Vue component and I want you to write it using Vue 3 to the best of your ability.&lt;/p&gt;

&lt;p&gt;write a simple single-file vue component using Vue 3 that does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;displays an image&lt;/li&gt;
&lt;li&gt;allows the users to click and drag to select a subsection of the image&lt;/li&gt;
&lt;li&gt;the subsection of the image is saved as a base64-encoded data url to a variable that is displayed below the image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The solution should make use of HTML canvas. When you click down on the image you begin selecting the subsection. You then move the mouse to make your subsection on the image, and when you mouse-up the subsection is selected and the data url is updated. Then the subsection is displayed at the very bottom of the page as a "preview" image using the base 64 image string as the image source.&lt;/p&gt;

&lt;p&gt;The selection box should be a dashed red line&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffp1hd0b5etbt52nh1h5h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffp1hd0b5etbt52nh1h5h.png" alt="RedLM Image Q&amp;amp;A" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This shows the final result of the UI I built for the image Q&amp;amp;A feature in RedLM. It uses a similar chat layout that the text-based Q&amp;amp;A feature uses, with the addition of the image preview included in the chat log. The user query in this example just says “Please describe the contents of the image”. This was the first image that I tested when building the image Q&amp;amp;A feature to see if the correct passage can be referenced based on the description of an image. This pulled the exact passage and the answer provides details about what happened (a fire broke out) where it happened (at the Gourd Temple) and why it happened (a Monk accidentally set an oil pot on fire).&lt;/p&gt;

&lt;p&gt;Here is a diagram showing the overall flow of data in the image Q&amp;amp;A feature:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsmdbertcctqcs0ird1ih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsmdbertcctqcs0ird1ih.png" alt="Diagram of RedLM Image Q&amp;amp;A with RAG and Vision Language Models" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This flow chart shows how the image Q&amp;amp;A feature works.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The user selects part of an image and writes a question. This data is then sent to the RedLM API as a post request to the &lt;code&gt;/mm-q-and-a&lt;/code&gt; endpoint (multi-modal Q&amp;amp;A).&lt;/li&gt;
&lt;li&gt;Vision language models are used to get a description of the image. Depending on the application configuration, this query can use models such as &lt;code&gt;Qwen/Qwen2-VL-2B-Instruct&lt;/code&gt; on RTX PCs or using the NVIDIA API Catalog using larger models such as &lt;code&gt;meta/llama-3.2-90b-vision-instruct&lt;/code&gt;. Not all vision language models have the same interface, so I added some logic to handle different model formats.&lt;/li&gt;
&lt;li&gt;The image description is used to fetch relevant documents from the Vector Database&lt;/li&gt;
&lt;li&gt;The full prompt with the image description and relevant documents is sent to the LLM. Again, inference for this step is done either with RTX PCs or using models from the &lt;code&gt;build.nvidia.com&lt;/code&gt; API catalog.&lt;/li&gt;
&lt;li&gt;The response from the LLM is sent back to the browser and is displayed to the user as a chat message.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is the prompt I used for the image Q&amp;amp;A feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Chinese prompt for image-based Q&amp;amp;A bot
&lt;/span&gt;&lt;span class="n"&gt;mm_q_and_a_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PromptTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;这是书中相关的内容：&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{context_str}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---------------------&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;下面是场景的描述：&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---------------------&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{image_description}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---------------------&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;根据上述的信息，尽量解释上说的场景和书的关系。&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# English prompt for image-based Q&amp;amp;A bot
&lt;/span&gt;&lt;span class="n"&gt;mm_q_and_a_prompt_english&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PromptTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Here is relevant content from the book:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{context_str}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---------------------&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Below is the description of a scene:&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---------------------&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{image_description}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;---------------------&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Based on the information provided above, try to explain the relationship between the described scene and the book content.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt engineering for this feature was tricky. I was able to get some awesome results that would give me detailed and accurate responses, and then sometimes the LLM would seem confused about my query and tell me that there was no relationship between the scene description and the book content. Sometimes it would give me an accurate description of the scene, but then proceed to tell me that the book content is not related to the scene at all.&lt;/p&gt;

&lt;p&gt;There is another important concept from LlamaIndex that I used to build the image Q&amp;amp;A feature: metadata filtering. Metadata filtering is an important concept in RAG systems  because it helps you focus your query on relevant documents in a precise way. A very simple example might be a RAG system that indexes news articles and stores the associated date as metadata. You could allow a user to set a date range for their query and only include articles that match the given date range.&lt;/p&gt;

&lt;p&gt;For my image Q&amp;amp;A system, I have a mapping between the paintings and their associated chapters. When I ask a question about a painting, I want to use the description of the image to find similar paragraphs, but only the paragraphs that occur in the painting’s associated chapter. What I ended up doing was filtering the entire index before making the query. The alternative would be filtering the returned nodes after making the query, but this would have the possibility of not returning any nodes.&lt;/p&gt;

&lt;p&gt;Here’s what some of the metadata filtering code looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# main.py
# filter by chapters associated with the queried image
&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MetadataFilters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;ExactMatchFilter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chapter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chapter&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;query_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_query_engine_for_multi_modal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# rag.py
# utility function that returns the query engine use for image Q&amp;amp;A
# the index is filtered to only include nodes associated with the image being queried
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_query_engine_for_multi_modal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;synthesizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_response_synthesizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response_mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;compact&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;query_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;QAndAQueryEngine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;response_synthesizer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;synthesizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;qa_prompt&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;mm_q_and_a_prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;query_engine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This seemed to work well for my use case, but it might not be a best practice, and it might not be efficient at a bigger scale.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multi-modal Q&amp;amp;A examples
&lt;/h3&gt;

&lt;p&gt;Here are some more examples of results from different types of questions from the multi-modal Q&amp;amp;A bot.&lt;/p&gt;

&lt;p&gt;The response to the following query did a good job of combining information gathered from the image description and image from related passages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feq64lb5l9kqkezxunbec.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feq64lb5l9kqkezxunbec.png" alt="Multi-modal Q&amp;amp;A example 2" width="800" height="462"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5fc3amzhh8lan3aglhi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl5fc3amzhh8lan3aglhi.png" alt="Multi-modal Q&amp;amp;A example 3" width="800" height="868"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdg16gqwj2mc8jkd24u32.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdg16gqwj2mc8jkd24u32.png" alt="Q&amp;amp;A Example with Carriage" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffc76gt72zx2a1qtiyij0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffc76gt72zx2a1qtiyij0.png" alt="Ou Guan Example" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is one of my favorite examples of the RedLM image Q&amp;amp;A bot in action. The query here in Chinese says: "What are these two people doing"? The answer incorporates a description of what is happening in the story (Jia Baoyu comes across Ou Guan while visiting a temple) and also describes the significance of what is happening (&lt;a href="https://en.wikipedia.org/wiki/Joss_paper" rel="noopener noreferrer"&gt;burning paper money as a form of Chinese ancestral worship&lt;/a&gt;). But this answer is not perfect and it demonstrates some of the difficulties I had with the prompt engineering for this project. The first part makes a lot of sense:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;这段参考资料来自中国古典小说《红楼梦》，讲述了贾宝玉在探访寺庙时发现藕官在河边烧纸钱的情景。藕官是贾府中的一个戏子，她的行为可能代表了对去世亲人的哀悼或祭奥。贾宝玉询问她烧纸的原因，藕官透露了自己有私下的情感问题，并且提到了芳官和蕊官，暗示了她们之间可能存在某种秘密或情感联系。&lt;/p&gt;

&lt;p&gt;This reference is from the classical Chinese novel Dream of the Red Chamber, describing a scene where Jia Baoyu discovers Ou Guan burning paper money by the river during his visit to a temple. Ou Guan is a performer in the Jia household, and her actions might represent mourning or honoring deceased loved ones. Jia Baoyu asks her why she is burning the paper, and Ou Guan reveals that she has private emotional issues, mentioning Fang Guan and Rui Guan, hinting that there might be some kind of secret or emotional connection among them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But the second part seems to contradict the first part of the response:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;相比之下，你描述的画作场景与《红楼梦》中的描述不同。画作描绘的是两个人在河边的场景，左边的人跪在地上，似乎在烧东西，右边的人站在旁边，似乎在观察或指导。根据你提供的描述，无法确定画中人物的具体行为，如可能是烧水、煮饭、烧药等。因此，这幅画可能是一幅现实主义作品，描绘了日常生活中的一幕。它可能没有像《红楼梦》中的情节那样深刻的情感或秘密背景，而是单纯地表现了人与人之间的关系和行为。此外，画中的背景、人物的表情和姿态等细节也会对画面的意义产生影响，需要结合具体的画作进行分析。&lt;/p&gt;

&lt;p&gt;In contrast, the scene you described in the painting differs from the depiction in Dream of the Red Chamber. The painting shows two people by the river: the person on the left is kneeling on the ground, seemingly burning something, while the person on the right stands nearby, possibly observing or guiding. Based on your description, it’s unclear what specific action the figures are engaged in, such as possibly boiling water, cooking, or burning herbs. Therefore, this painting might be a realist work, portraying a moment from everyday life. It may not have the profound emotional or secretive background found in the storyline of Dream of the Red Chamber, instead simply highlighting the relationships and actions between individuals. Additionally, details such as the background, expressions, and postures of the figures in the painting would also influence the scene’s meaning and would require analysis based on the specific artwork.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  LlamaIndex Developer Experience
&lt;/h2&gt;

&lt;p&gt;Overall, I found the LlamaIndex documentation to be very helpful. Before using LlamaIndex for this project I had used LangChain to build a RAG POC, but I didn’t get very good results. I love how the LlamaIndex documentation has a 5-line starter example for building a RAG system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;llama_index.core&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;VectorStoreIndex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SimpleDirectoryReader&lt;/span&gt;

&lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;SimpleDirectoryReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;load_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VectorStoreIndex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_documents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;query_engine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_query_engine&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;query_engine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Some question about the data should go here&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source: &lt;a href="https://docs.llamaindex.ai/en/stable/#getting-started" rel="noopener noreferrer"&gt;https://docs.llamaindex.ai/en/stable/#getting-started&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was able to expand this simple example to implement the text and image Q&amp;amp;A bots for RedLM fairly easily. The application I built is somewhat similar to the &lt;a href="https://docs.llamaindex.ai/en/stable/understanding/putting_it_all_together/apps/fullstack_app_guide/" rel="noopener noreferrer"&gt;Full-Stack Web App with LLamaIndex&lt;/a&gt; included in their documentation.&lt;/p&gt;

&lt;p&gt;Most of the early development I did on this project used the &lt;code&gt;CustomQueryEngine&lt;/code&gt;. Later I tried using &lt;a href="https://docs.llamaindex.ai/en/stable/module_guides/workflow/" rel="noopener noreferrer"&gt;LlamaIndex Workflows&lt;/a&gt; to better organize the logic in the text and image-based Q&amp;amp;A bots. The same workflow &lt;code&gt;RAGWorkflow&lt;/code&gt; is used to handle requests for both the text and image Q&amp;amp;A bot queries. Workflows also work seamlessly with asynchronous Python frameworks like FastAPI. Here's the API endpoint for the multimodal image-Q&amp;amp;A bot using a LlamaIndex Workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/mm-q-and-a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;mm_q_and_a_workflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;MultiModalRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    This function handles Multimodal Q&amp;amp;A bot requests using a LlamaIndex workflow
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# parse data from request object
&lt;/span&gt;        &lt;span class="n"&gt;image_b64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;
        &lt;span class="n"&gt;prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;
        &lt;span class="n"&gt;chapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req_data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chapter&lt;/span&gt;

        &lt;span class="c1"&gt;# setup LlamaIndex Workflow and run it with data from request
&lt;/span&gt;        &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RAGWorkflow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;image_b64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chapter_number&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# return response
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;QAQueryResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;metadata&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;image_desc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image_description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using LlamaIndex Workflows also helped me add additional logic in a maintainable and standardized way. For example, I expanded the &lt;code&gt;RAGWorkflow&lt;/code&gt; logic to include LLM-based re-ranking in order to ensure retrieval of the most relevant documents for my chatbot queries. This technique increases request latency, but this is an acceptable tradeoff for an application like RedLM.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLMRerank
&lt;/h3&gt;

&lt;p&gt;LLM Rerank was an interesting technique to try out, and LlamaIndex provides &lt;code&gt;LLMRerank&lt;/code&gt; to make the implementation as simple as possible. Here's my understanding of how it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;LLMRerank searches in the vector database for a high number of documents that are relevant to your query. This is done using cosine similarity, which essentially compares the vectors that represent the query and the documents.&lt;/li&gt;
&lt;li&gt;Next, LLMRerank goes through a process of assigning a numerical to each document to score relevancy. It does this via a special prompt that requests relevancy score for each document in batches.&lt;/li&gt;
&lt;li&gt;For example, I configured &lt;code&gt;LLMRerank&lt;/code&gt; to initially fetch 4 documents from the vector database based on cosine similarity. Then in batches of 2, relevancy scores are assigned. Finally, the top 2 most relevant documents based on the LLM-give scores are used to make the RAG query.&lt;/li&gt;
&lt;li&gt;Adding LLMRerank can require a number of additional queries based on how you configure the batch size and the number of documents you would like to compare. This will increase latency for your application and use more resources to make the extra calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's an example LLM query that &lt;code&gt;LLMRerank&lt;/code&gt; uses to do assign scores:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fomap0yhig6pfuxij1a5e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fomap0yhig6pfuxij1a5e.png" alt="LLMRerank Prompt" width="800" height="552"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here are logs from my application showing what happens inside the workflow.&lt;/p&gt;

&lt;p&gt;Application for text-base Q&amp;amp;A query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO:     💬Request for Q&amp;amp;A chatbot: query='宝玉和谁打架？'
INFO:     🔀Routing Workflow to next step
INFO:     💬Routing to QueryEvent
INFO:     🧮Query the vector database with: 宝玉和谁打架？
INFO:     🖥️Using in-memory embedding database
INFO:     ⏳Loading index from storage directory...
INFO:     ✅Finished loading index.
INFO:     📐Retrieved 4 nodes.
INFO:     🔀Doing LLMRerank
INFO:     ℹ️ Chat Model Info:
INFO:     🟩Using NVIDIA Cloud API for inference
INFO:     🔘Chat Model: baichuan-inc/baichuan2-13b-chat
INFO:     🔢Reranked nodes to 2
INFO:     🤖Doing inference step
INFO:     ⚙️ Getting query engine..
INFO:     🔎Getting response from custom query engine
INFO:     💬Text-based Q&amp;amp;A query
INFO:     🀄Text is Chinese
INFO:     Using nodes from workflow...
INFO:     🔏Formatting prompt
INFO:     Prompt is

这是相关的参考资料：
---------------------
宝玉从来没有经历过这样的痛苦。起初，他觉得被打得很痛，乱喊乱叫。后来，他的气变得虚弱，声音变得嘶哑，无法说话。众门客见他被打得很惨，赶上来恳求他停下来。贾政不肯听，说：“你们知道他干了什么坏事，还能饶他吗？平时都是你们这些人把他带坏了，现在到了这步田地，你们还来劝他。明天，如果他杀父弑君，你们才不劝吗？”

宝玉从来没有经历过这样的痛苦。起初，他觉得打得很痛，乱喊乱叫。后来，他的气变得虚弱，声音变得嘶哑，无法说话。众门客见他被打得很惨，赶上来恳求他停下来。贾政不肯听，说：“你们知道他干了什么坏事，还能饶他吗？平时都是你们这些人把他带坏了，现在到了这步田地，你们还来劝他。明天，如果他杀父弑君，你们才不劝吗？”
---------------------
根据上述的参考资料，回答下面的问题
问题：宝玉和谁打架？

Response...
宝玉和贾政打架。
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My question here was basically asking "Who gets in a fight with Baoyu?" The reply says that his father, Jiazheng, gets in a fight with Baoyu, and the documents that are used here very similar, differing by only one character. One of the documents is supposed to be and English translation, but in fact there was a failure in the translation for this paragraph and it "translated" the Chinese by simply repeating it. A translation of this paragraph using GPT 4o describes a tense scene between protagonist Jia Baoyu and his father Jia Zheng:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Baoyu had never endured such agony before. At first, he felt the pain intensely and cried out loudly. Later, his breath grew weak, his voice turned hoarse, and he couldn’t speak. The attendants, seeing how severely he was being beaten, rushed forward to plead for him to stop. Jia Zheng refused to listen, saying, “Do you know the misdeeds he’s committed, and still you want to spare him? Normally, it’s you people who lead him astray, and now that it’s come to this, you still try to persuade him? Tomorrow, if he were to commit patricide or treason, would you still not advise him?”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another benefit of LlamaIndex workflows is the ability to create visualizations of each step, the branches between them and the overall flow of events and the functions that accept/emit them as arguments/return values. It took a little bit of getting used to the patterns used to create workflows, but the documentation for Workflows provided a good starting point that I could adapt for my application. Here's a visualization of the LlamaIndex Workflow that is used by the image and text-based Q&amp;amp;A bots:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F35iwre5192a5pcp91d1f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F35iwre5192a5pcp91d1f.png" alt="RedLM RAG Workflow" width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Observability and Tracing with Langfuse
&lt;/h3&gt;

&lt;p&gt;It is never too soon to add observability and tracing to a RAG application! I learned this the hard way after doing some refactoring of prompts and &lt;code&gt;CustomQueryEngine&lt;/code&gt; logic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Langfuse is an open source LLM engineering platform to help teams collaboratively debug, analyze and iterate on their LLM Applications. With the Langfuse integration, you can seamlessly track and monitor performance, traces, and metrics of your LlamaIndex application. Detailed traces of the LlamaIndex context augmentation and the LLM querying processes are captured and can be inspected directly in the Langfuse UI.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;LlamaIndex supports lots of different observability and tracing solutions. I tried using &lt;a href="https://langfuse.com/" rel="noopener noreferrer"&gt;Langfuse&lt;/a&gt; (YC W23) which is an open-source option that has a self hosted option.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fre9jk7veti5auzbjcf1d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fre9jk7veti5auzbjcf1d.png" alt="Langfuse tracing for RedLM" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Langfuse came in handy when debugging the prompts for the image-based Q&amp;amp;A bot. This screenshot shows a trace of a multi-modal Q&amp;amp;A bot query about the fire at the Gourd Temple that occurs in Chapter 1 of the book.&lt;/p&gt;

&lt;h2&gt;
  
  
  NVIDIA inference stack (TensorRT-LLM and build.nvidia.com)
&lt;/h2&gt;

&lt;p&gt;The LLM API for TensorRT-LLM is a very nice developer experience compared with my earlier attempts with manually building inference engines. The roadmap for TensorRT-LLM looks promising, I’m looking forward to support for an OpenAI Compatible API and more models. NVIDIA NIMs using TensorRT-LLM are an easy way to run models as OpenAI compatible API servers, but the selection of models is still pretty limited. vLLM provides a strong alternative with a wide range of support models. NVIDIA NIMs for LLMs build on vLLM libraries and the TensorRT-LLM library, so it is helpful to have an understanding of both of these libraries to stay on the bleeding edge of performant inference engines.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9kkeib2vjv9edjw7h1d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fl9kkeib2vjv9edjw7h1d.png" alt="trt-llm-roadmap" width="800" height="496"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The NVIDIA API catalog is a great way to test a variety of different models, especially large models that cannot fit into consumer hardware like RTX PCs or high-end MacBooks. I got to try out the new meta/llama-3.2-90b-vision-instruct model in my project by simply changing a value in my .env file, this is a great developer experience!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flar42nctlxa0knvom03d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flar42nctlxa0knvom03d.png" alt="build.nvidia.com" width="800" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The NVIDIA API catalog doesn’t have every model in every size, however. For example, it has the qwen/qwen2-7b-instruct model, but doesn’t have the qwen/qwen2-7b-instruct model. Also, only some of the models are labeled as “Run Anywhere”; a lot of the models say “Self-Hosted API Coming Soon” meaning that they can’t be downloaded an run locally as a container. To get around this, I ran inferences services locally using both vLLM’s vllm/vllm-openai container and my own container running Qwen and other services.&lt;/p&gt;

&lt;h2&gt;
  
  
  My local inference stack (RTX)
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwi8dqjpxbpe40okjegp0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwi8dqjpxbpe40okjegp0.png" alt="RTX PCs" width="800" height="384"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two of the RTX PCs in my home network: &lt;code&gt;a1&lt;/code&gt; and &lt;code&gt;a3&lt;/code&gt;. &lt;code&gt;a1&lt;/code&gt; was the first PC I built by myself and was the beginning of my GeForce journey. Luckily I built it with an over-provisioned PSU, so it can use a 4090 FE card! The front panel doesn't fit, however.&lt;/p&gt;

&lt;p&gt;One limitation of the NVIDIA API catalog is the number of free credits given for a trial account. Using 1 credit per API call, I would use up the 1000 credits very quickly when running scripts like translation or the RAG evaluation. The same would be true with rate limits of the OpenAI API. That’s why running LLMs locally is still an important part of the development cycle for this type of project.&lt;/p&gt;

&lt;p&gt;This project primarily uses two models: a large language model and a vision language models. Running the Yi-1.5-9B-Chat model from &lt;a href="http://01.AI" rel="noopener noreferrer"&gt;01.AI&lt;/a&gt; takes up just about all of the GPU memory on one of my RTX 4090 PCs, so I had to run the vision model on another PC. In a previous project, I used Kubernetes to manage lots of different inference services: LLMs, ComfyUI, ChatTTS and MusicGen for making AI videos and I found it to a nice way to manage different containerized inference services.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;brian@a3:~$ microk8s kubectl get no -o wide
NAME   STATUS   ROLES    AGE    VERSION   INTERNAL-IP     EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
a1     Ready    &amp;lt;none&amp;gt;   4d4h   v1.30.5   192.168.5.182   &amp;lt;none&amp;gt;        Ubuntu 24.04.1 LTS   6.8.0-45-generic   containerd://1.6.28
a2     Ready    &amp;lt;none&amp;gt;   11d    v1.30.5   192.168.5.96    &amp;lt;none&amp;gt;        Ubuntu 24.04.1 LTS   6.8.0-45-generic   containerd://1.6.28
a3     Ready    &amp;lt;none&amp;gt;   11d    v1.30.5   192.168.5.173   &amp;lt;none&amp;gt;        Ubuntu 24.04.1 LTS   6.8.0-45-generic   containerd://1.6.28
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the RedLM GitHub repo I included kubernetes manifests that show how to run the LLM and VLM across two different computers. I used Kustomize as a way to replace dynamic values in the YAML files for different resources. The kubernetes set up is experimental; the LLM and VLM can more reliably be run with &lt;code&gt;docker run&lt;/code&gt; commands.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25z3244ktwjtiwr1x9y7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F25z3244ktwjtiwr1x9y7.png" alt="k8s dashboard for local inference services" width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I had a lot of driver issues when trying to get kubernetes to run the vLLM container for the Yi LLM. I struggled with the following error message when trying to run the &lt;code&gt;vllm&lt;/code&gt; LLM service:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;RuntimeError: Unexpected error from cudaGetDeviceCount(). Did you run some cuda functions before calling NumCudaDevices() that might have already set an error? Error 804: forward compatibility was attempted on non supported HW&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I tried uninstalling and reinstalling different versions of the NVIDIA drivers and CUDA but kept seeing the same message once the server would try to start up in the vLLM container logs. Rebooting my PC didn't work either. I saw a recommendation to turn off secure boot in BIOS. I didn't turn it on, but having nothing else to try I went into the BIOS settings and found that there were some keys configured in the secure boot section. After I deleted these keys and reboot, everything seemed to work normally. I'm not sure why my PC was in secure boot mode, though!&lt;/p&gt;

&lt;h2&gt;
  
  
  AI Models used in this project
&lt;/h2&gt;

&lt;p&gt;I selected LLMs that run efficiently on RTX PCs, are available in the NVIDIA API catalog, and offer strong bilingual support in Chinese and English, ensuring compatibility, performance, and linguistic flexibility. Here are the models that I ended up using with RedLM:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;01-ai/Yi-1.5-9B-Chat&lt;/code&gt; and &lt;code&gt;nvidia/yi-large&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;I used &lt;code&gt;01-ai/Yi-1.5-9B-Chat&lt;/code&gt; for most of the LLM inference while developing RedLM on my RTX PCs. &lt;a href="https://github.com/01-ai/Yi" rel="noopener noreferrer"&gt;This model family&lt;/a&gt; performs well on both Chinese and English benchmarks, and has a variety of model sizes. I was able to try using the &lt;code&gt;01-ai/yi-large&lt;/code&gt; model from the NVIDIA API catalog when using remote cloud inference. I used the &lt;code&gt;vllm/vllm-openai:latest&lt;/code&gt; container to run this locally.&lt;/p&gt;

&lt;p&gt;There are also vision models in the Yi series, such as &lt;a href="https://huggingface.co/01-ai/Yi-VL-34B" rel="noopener noreferrer"&gt;01-ai/Yi-VL-34B&lt;/a&gt;, but I didn't use these models in my project.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;baichuan-inc/baichuan2-13b-chat&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This model is available in the NVIDIA API catalog, and it was the main model I used when testing remote inference. It performs well in a variety of tasks and scores highly on the the Chinese Massive Multitask Language Understanding (CMMLU) benchmark.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Qwen/Qwen2-7B&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This model was used for summary and translation of the source text. It was supported by the TensorRT-LLM LLM API and I didn't have any issues building the TensorRT-LLM model with it on the EC2 instance used to do the completion inference for translations.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Qwen/Qwen2-VL-2B-Instruct&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This was the vision language model (VLM) that I used locally when developing on RTX. I was impressed at how well it could describe images given the small parameter count of the model (2 billion parameters). The small size of this model made it easy to run in my RTX PC cluster.&lt;/p&gt;

&lt;p&gt;There is &lt;a href="https://github.com/NVIDIA/TensorRT-LLM/issues/2183" rel="noopener noreferrer"&gt;an open GitHub issue for TensorRT-LLM support for Qwen2-VL&lt;/a&gt; at the time of writing.&lt;/p&gt;

&lt;p&gt;I wrote a simple FastAPI server using the Hugging Face &lt;code&gt;transformers&lt;/code&gt; library based on example code from this model's documentation (see &lt;code&gt;services/qwen2-vl&lt;/code&gt; in the RedLM GitHub repo for more details). I packaged this service into a container in order to run it in my local kubernetes cluster along with other inference services.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;meta/llama-3.2-90b-vision-instruct&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This model came out while I was working on the project, and I decided to use it instead of the &lt;code&gt;adept/fuyu-8b&lt;/code&gt; model that was previously one of the only vision language models in the NVIDIA API catalog. The &lt;code&gt;meta/llama-3.2-90b-vision-instruct&lt;/code&gt; model has strong Chinese language skills, so it was a good model to use when doing remote inference for the image Q&amp;amp;A bot.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://huggingface.co/nvidia/NVLM-D-72B" rel="noopener noreferrer"&gt;&lt;code&gt;nvidia/NVLM-D-72B&lt;/code&gt;&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;I didn't use this model in my project, but it came out recently and looks awesome! Hopefully this model will be available on the NVIDIA API catalog soon. It is trained on the &lt;code&gt;Qwen2-72B-Instruct&lt;/code&gt; text-only model, so it likely also has very strong support for Chinese language.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Today (September 17th, 2024), we introduce NVLM 1.0, a family of frontier-class multimodal large language models (LLMs) that achieve state-of-the-art results on vision-language tasks, rivaling the leading proprietary models (e.g., GPT-4o) and open-access models (e.g., Llama 3-V 405B and InternVL 2). Remarkably, NVLM 1.0 shows improved text-only performance over its LLM backbone after multimodal training.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The success of Black Myth: Wukong
&lt;/h2&gt;

&lt;p&gt;I originally got the idea to build this project after seeing the release of Black Myth: Wukong. This game is a blockbuster success from a Chinese developer that tells the story of the Monkey King’s adventure in the Journey West universe. Journey West (西游记) is another one of the “Four Great Works” of Chinese literature. It tells the story of the legendary pilgrimage of the monk Xuanzang (also known as Tang Sanzang) to India, accompanied by his three disciples—Sun Wukong (the Monkey King), Zhu Bajie (Pigsy), and Sha Wujing (Sandy). The group travels from China to India to retrieve sacred Buddhist scriptures, facing numerous challenges, demons, and supernatural beings along the way.&lt;/p&gt;

&lt;p&gt;The novel blends elements of adventure, mythology, and spiritual allegory, with Sun Wukong's mischievous nature and extraordinary powers adding humor and excitement. Through their journey, the characters grow and overcome personal flaws, ultimately achieving enlightenment and spiritual success. The video game adaptation has set world records for numbers of concurrent players, and it has rewritten the narrative around what is possible with single-player, offline games in the gaming industry.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc74lo20pa8xep52xq7t0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc74lo20pa8xep52xq7t0.png" alt="Black Myth: Wukong" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Three renditions of Journey West: Songokū (The Monkey King) polychrome woodblock (surimono) (1824) by Yashima Gakutei (1786–1868), Black Myth: Wukong video game by Game Science (2024), Journey to the West TV series by CCTV (1982-2000)&lt;/p&gt;

&lt;h2&gt;
  
  
  RedLM video
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://x.com/briancaffey/status/1855186768452321330" rel="noopener noreferrer"&gt;Watch the RedLM video on 𝕏&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;I created the video for this project using Blender.The Blender sequencer editor is a great non-linear video editing tool for simple video projects like this one. I used the following formula to create the project video for RedLM:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Background music: I used the AI music generation service called Suno with the prompt “mystical strange traditional Chinese music from the Qing Dynasty”. Here’s the link to my Suno playlist called “Qing Dynasty Music” where you can find the original song and some other good songs that I generated using this prompt. My &lt;a href="https://suno.com/playlist/863ea0dd-1921-467c-8b69-16dbd126d966" rel="noopener noreferrer"&gt;Qing Dynasty Music Playlist on Suno&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Outline: For this project, the main sections are the introduction, then explaining each part with a short demo: translation, text-based Q&amp;amp;A, evaluation for text-based Q&amp;amp;A, image-based Q&amp;amp;A, and finally a short outro. I wrote an outline and then ChatGPT helped with filling out the content.&lt;/li&gt;
&lt;li&gt;Narration: I used ElevenLabs to narrate the main part of the video using a clone of my voice using the ElevenLabs Voice Lab. The Chinese voices were generated on my computer with an open-source text-to-speech model called ChatTTS.&lt;/li&gt;
&lt;li&gt;Images and videos: I gathered images and screen captures of different parts of the project including code snippets, paintings of the book, flow diagrams and screen recordings of the application.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The video is composed of different “strips”. The green strips represent the music and voice clips. Red strips are images and yellow strips are videos. Here is what the final cut of the video looks like in Blender’s Sequencer view:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fot64s5eviiyysy38jwst.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fot64s5eviiyysy38jwst.png" alt="Blender Sequence Editor" width="800" height="326"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ChatTTS is one of the most impressive open-source models I have seen for generating conversational speech with prosodic elements (pausing, laughter, etc.) It is developed by a Chinese company called 2noise. Earlier this year I made a small contribution to this project with an API example using FastAPI to show how to run a standalone API using the model. Another example in this project provides a comprehensive example application built with gradio:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foie19lfsnzf164m6x68o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foie19lfsnzf164m6x68o.png" alt="ChatTTS UI" width="800" height="826"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was planning on streaming the narration audio for Q&amp;amp;A answers using my ChatTTS API service, but I didn’t get around to doing this. Instead, I just used the Gradio application to generate the Chinese narration for Q&amp;amp;A and image Q&amp;amp;A examples included in the video.&lt;/p&gt;

&lt;h3&gt;
  
  
  RedLM Deep Dive video with NotebookLM
&lt;/h3&gt;

&lt;p&gt;NotebookLM is a new application from Google that is a truly magical application of retrieval augmented generation.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;NotebookLM is a research and note-taking online tool developed by Google Labs that uses artificial intelligence, specifically Google Gemini, to assist users in interacting with their documents. It can generate summaries, explanations, and answers based on content uploaded by users.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I used NotebookLM to generate a "Deep Dive" podcast episode using only this article. I was pretty impressed with what it was able to produce, and I wanted to share it as part of this project, so I used Blender and some Python scripts to put together a simple and engaging visualization.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmn1u4khie83we1q7jbrj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmn1u4khie83we1q7jbrj.png" alt="Deep Dive video in Blender" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;openai/whisper-base&lt;/code&gt; model was used to get time stamps for the start and end of each spoken word using Automated Speech Recognition (ASR). A speaker segmentation library called &lt;a href="https://github.com/pyannote/pyannote-audio" rel="noopener noreferrer"&gt;&lt;code&gt;pyannote/audio&lt;/code&gt;&lt;/a&gt; was used to perform speaker diarization. This is an interesting algorithm that can segment any number of distinct speakers in an audio recording using a series of models and a discrete-time stochastic process known as the &lt;a href="https://en.wikipedia.org/wiki/Chinese_restaurant_process" rel="noopener noreferrer"&gt;Chinese restaurant process&lt;/a&gt;. This gave a list of time intervals with a speaker ID, and I used the intervals to attribute a speaker ID to each word. Then I segmented the audio into two files using this data and used the files to generate audio waveforms using Blender's geometry nodes. Another script was used to animate each word of as it is spoken in one of two positions for each speaker.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;I’m glad to have had the opportunity to join three NVIDIA developer contests this year. I like the idea of a “developer contest” that takes place over several weeks compared to hackathons that take place over just a few days. Having more time allows you to learn about a new tool or framework at a deeper level and think about how to apply it in a creative project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsxby2ezi4js276j4wrf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frsxby2ezi4js276j4wrf.jpg" alt="NVIDIA and LlamaIndex Contest" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also like how this contest is not team based. Working on this project I was able to do a lot of high-level thinking, write out features as detailed prompts, and then delegate the code writing to LLMs as if I was giving tasks to teammates.&lt;/p&gt;

&lt;p&gt;NVIDIA’s contests are “global developer contests”, but the contests so far are not open to developers in India and China. This is probably due to local rules and regulations governing how contests, prizes and taxes work. It is too bad; I would love to see what types of applications would come from participants in these countries. Also, there are also a lot of really interesting developments happening in the LLM space in both China and India!&lt;/p&gt;

&lt;p&gt;The LLMs I used in this project were developed by leading Chinese AI companies, and they are competitive with LLMs from Western countries on LLM benchmarks despite having access to fewer GPU resources. &lt;a href="https://qwenlm.github.io/blog/qwen2.5-coder-family/" rel="noopener noreferrer"&gt;Qwen recently released a new model called &lt;code&gt;Qwen2.5-Coder-32B&lt;/code&gt;&lt;/a&gt; that has outperfomed leading models at coding tasks.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmygwfhkpv47pxs9sn9dz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmygwfhkpv47pxs9sn9dz.png" alt="Qwen coder model" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=UitJxc9LE60" rel="noopener noreferrer"&gt;Kaifu Lee mentioned in a Bloomberg interview&lt;/a&gt; that the scarcity of GPU resources in China will force Chinese engineers to innovate in new ways to gain an advantage. One example of this we saw recently was when Chinese hardware hackers doubled the usable memory of the RTX 4090D (a variant of the RTX 4090 card with lower processing power to comply with US export regulations for China - the D stands for Dragon, apparently!)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fygb8ogvfrbkhb4va1ko0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fygb8ogvfrbkhb4va1ko0.jpg" alt="RTX 4090D 48GB" width="800" height="416"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NVIDIA recently concluded it's AI Summit in Mumbai. I was intrigued by the fact that Hindi has unique challenges that have have limited the development of Hindi LLMs compared to the development of English and Chinese LLMs. In a conversation with Jensen Huang, Indian industrial titan and CEO of Reliance Industries Mukesh Ambani spoke about his aspirations and ambition for India to overcome these challenges and develop a Hindi LLM. In a viral moment Mukesh Ambani shared that through devotion to attaining knowledge through the Hindu Goddess of knowledge Sarawati, India will be met by the Goddess of prosperity, Lakshmi.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xzaol0ffuonccgiw4if.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4xzaol0ffuonccgiw4if.png" alt="Mukesh Ambani" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;NVIDIA recently released a small language model for Hindi at the AI Summit in Mumbai called  &lt;a href="https://indiaai.gov.in/article/nvidia-unveils-nemotron-4-mini-hindi-4b-ai-for-india-s-500-million-hindi-speakers" rel="noopener noreferrer"&gt;Nemotron-4-Mini-Hindi-4B&lt;/a&gt;. Hindi LLMs could enable applications to explore important works of literature from India. I don't know that much about India literature, but a comparable work of literature in size and cultural significance might be the Ramayana.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The Ramayana is an ancient Indian epic that tells the story of Prince Rama's heroic quest to rescue his wife, Sita, who has been kidnapped by the demon king Ravana. Set in a world of gods, demons, and celestial beings, the story explores themes of duty, loyalty, and the triumph of good over evil. Guided by wisdom, strength, and the support of devoted allies like Hanuman, the monkey god, and his brother Lakshmana, Rama's journey is a deeply spiritual tale, celebrated for its poetic beauty and moral depth. The Ramayana continues to inspire and captivate audiences across cultures.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The Ramayana story journeyed to Thailand centuries ago, transforming into the Ramakien, a Thai adaptation that retains the essence of the original Indian epic while adding distinctive Thai cultural elements. Introduced through trade, diplomacy, and cultural exchange between India and Southeast Asia, the story became deeply woven into Thailand’s art, literature, and performance traditions. Thai kings, particularly King Rama I, adapted and documented the Ramakien, giving it a prominent place in Thai history. Lavishly detailed murals surrounding the Temple of the Emerald Buddha in Bangkok’s Grand Palace depict the Ramakien in over 178 panels that totaling over 2 kilometers in length. On a recent visit to the Grand Palace, I imagined having an application that could link the detailed murals to elements of the story in Hindi, Thai, English, Chinese or any language.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9m1ahlb7ksjris4sgdwj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9m1ahlb7ksjris4sgdwj.png" alt="Ramakien murals surrounding Temple of the Emerald Buddha" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Dream of the Red Chamber, originally titled The Story of the Stone, is one of China’s greatest literary works and a masterpiece of world literature. The novel begins with a frame story centered on a magical stone, left over from the Chinese creation myth where the goddess Nuwa mends the heavens. Longing to experience the human world, the sentient stone persuades a Buddhist monk and a Taoist priest to reincarnate it as a boy. This boy, Baoyu, is born into a wealthy and influential family—a character partly based on the author, Cao Xueqin, and his own aristocratic upbringing. Through Baoyu's life, friendships, and romantic relationships, the novel delves into his family’s gradual decline, mirroring the instability of China’s own noble families in the late Qing dynasty. The story also portrays the era's customs, social structures, and beliefs, offering readers a richly detailed exploration of life in Qing China.&lt;/p&gt;

&lt;p&gt;It was a lot of fun to work on this project with tools from LlamaIndex and NVIDIA. With AI technology, GPUs are now essentially sentient stones, and I was able to share this important touchstone of the human experience with my computers using LlamaIndex and open source language models. In turn, RedLM shared with me delightful insights into world of Dream of the Red Chamber.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk9phha5plq5ek7znieq1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk9phha5plq5ek7znieq1.png" alt="Story of a Stone" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpj7ss28smubekumayev6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpj7ss28smubekumayev6.png" alt="Story of a Stone Analysis" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This scene describes a piece of traditional Chinese painting, depicting two elderly figures conversing amidst mountains and rivers. The painting likely visually represents the scene from the book where a monk and a Taoist are chatting at the foot of Qinggeng Peak. The two elderly figures in the painting may represent the monk and Taoist from the book, discussing their discovery of a bright and pristine stone, and planning to take it to a bustling, splendid place for a happy life. The painting’s elements—mountains, peaks, flowing water, trees, and rocks—might echo the book's descriptions, illustrating the natural environment at the base of Qinggeng Peak where the monk and Taoist reside. The painting’s tranquil and harmonious atmosphere may also align with the storyline, expressing the monk and Taoist's care for the stone and their wish for it to live a happy life. In summary, this painted scene might be an artistic portrayal of the story between the monk, the Taoist, and the stone from the book, using visual elements and ambiance to convey the narrative and themes within the story.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>nvidia</category>
      <category>llamaindex</category>
      <category>llm</category>
      <category>rag</category>
    </item>
    <item>
      <title>Rocket League BotChat powered by TensorRT-LLM: My submission for NVIDIA's Generative AI on RTX PCs Developer Contest</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Mon, 19 Feb 2024 12:42:16 +0000</pubDate>
      <link>https://dev.to/briancaffey/rocket-league-botchat-powered-by-tensorrt-llm-my-submission-for-nvidias-generative-ai-on-rtx-pcs-developer-contest-2oao</link>
      <guid>https://dev.to/briancaffey/rocket-league-botchat-powered-by-tensorrt-llm-my-submission-for-nvidias-generative-ai-on-rtx-pcs-developer-contest-2oao</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3k19s72ymm4671nz25b9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3k19s72ymm4671nz25b9.png" alt="Rocket League BotChat" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;This article is about my submission to NVIDIA's Generative AI on RTX PCs Developer Contest: Rocket League BotChat. Rocket League BotChat is a BakkesMod plugin for Rocket League that allows bots to send chat messages based on in-game events. It is designed to be used with a local LLM service optimized and accelerated with NVIDIA's TensorRT-LLM library.&lt;/p&gt;

&lt;p&gt;Here's my project submission post on 𝕏:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1760529251072118901-463" src="https://platform.twitter.com/embed/Tweet.html?id=1760529251072118901"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1760529251072118901-463');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1760529251072118901&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;Here's a link to the Rocket League BotChat GitHub repository.&lt;/p&gt;

&lt;p&gt;NVIDIA's Gen AI Developer Contest&lt;br&gt;
The following email caught my attention last month:&lt;/p&gt;

&lt;p&gt;Generative AI on RTX PCs Developer Contest: Build your next innovative Gen AI project using NVIDIA TensorRT or TensorRT-LLM on Windows PC with NVIDIA RTX systems&lt;/p&gt;

&lt;p&gt;The part about “on Windows PC” made me think: why would a developer contest focus on a particular operating system? I use all three of the major operating systems: macOS, Ubuntu and Windows 11, but most of the development work I do is on macOS and Ubuntu. I discovered WSL (Windows Subsystem for Linux) a few years ago and really enjoy using that for development as well, but I had never considered doing development work on Windows outside of WSL. I had also never used any of the Windows-specific development frameworks like .NET or Visual Studio.&lt;/p&gt;

&lt;p&gt;My experience with Windows goes back to 2016 when I built my fist PC with an NVIDIA GeForce GTX 1080 graphics card. When I built another personal computer last year in 2023, getting the NVIDIA GeForce RTX 4090 graphics card was a big step up. I bought two NVMe drives in order to dual boot into both Windows and Ubuntu operating systems. Switching between the operating systems requires turning off the computer, going into the BIOS settings and changing the boot order and restarting the computer.&lt;/p&gt;

&lt;p&gt;Last year I started learning more about AI image generation using Stable Diffusion with programs like Automatic1111, InvokeAI and ComfyUI. I set up everything on my PC's Ubuntu operating system, and frequently had to switch between using Ubuntu for working with stable diffusion and Windows for gaming and other Windows-specific software. The friction of having to constantly switch operating systems pushed me to move my stable diffusion software workflows to Windows. All of my models and images are stored to external drives, so moving things over to Windows was pretty easy.&lt;/p&gt;

&lt;p&gt;I learned PowerShell and got more familiar with how Windows works as a development machine. Environment variables and system variables are one example of how Windows does things differently compared ot Linux-based operating systems. And just like that, I became a Windows developer! This experience got me interested in coming up with an idea for the NVIDIA Generative AI on NVIDIA RTX PCs Developer Contest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9puzo6ofxsa6c9w6s56.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv9puzo6ofxsa6c9w6s56.png" alt="winfetch" width="800" height="459"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Coming up with an Idea&lt;br&gt;
The contest description and some related NVIDIA articles about the contest helped me with brainstorming:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Whether it’s a RAG-based chatbot, a plug-in for an existing application, or a code generation tool, the possibilities are endless.&lt;/p&gt;

&lt;p&gt;Many use cases would benefit from running LLMs locally on Windows PCs, including gaming, creativity, productivity, and developer experiences.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This contest is focused on NVIDIA's consumer hardware line: GeForce RTX. It has a diverse set of use cases including gaming, crypto mining, VR, simulation software, creative tools and new AI techniques including image generation and LLM (Large Language Model) inference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fg.foolcdn.com%2Fimage%2F%3Furl%3Dhttps%253A%252F%252Fg.foolcdn.com%252Feditorial%252Fimages%252F764886%252Fnvda_revenue_bar.png%26op%3Dresize%26w%3D700" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fg.foolcdn.com%2Fimage%2F%3Furl%3Dhttps%253A%252F%252Fg.foolcdn.com%252Feditorial%252Fimages%252F764886%252Fnvda_revenue_bar.png%26op%3Dresize%26w%3D700" alt="A stacked bar chart showing the composition of Nvidia's revenue each quarter going back to fiscal 2019" width="700" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gaming seemed like an interesting avenue for me to explore. PC gaming is still an industry that is developed primarily for Windows operating systems, and the gaming industry has been the largest revenue driver of NVIDIA in recent years, only recently surpassed by the data center segment. GPUs are needed to render graphics of enormous open-world environments. Some story-driven games include huge amounts of dialogue that can be considered as huge literary works in their own right. Red Dead Redemption and Genshin Impact are two massively popular games of this type. There might be an interesting project idea that could use LLMs and RAG (retrieval augmented generation), but I don't play these types of games and it didn't seem practical for a project that would be built in just over a month. I thought about trying to build something for a simpler game that I already know.&lt;/p&gt;

&lt;p&gt;Rocket League is a vehicular soccer game that is played on both game consoles and on PCs. It is an eSports with a very high skill ceiling and a massive player base (85 million active players in the last 30 days). I started playing it during the pandemic with some of my friends and all got hooked. We also came to learn that Rocket League's in-game is varies from entertaining, annoying, toxic and in some cases, sportsmanlike.&lt;/p&gt;

&lt;p&gt;One other thing I learned about Rocket League is that it has an active modding community. Developers create plugins for the game for all different purposes, such as coaching, practice drills, capturing replays, tracking player statistics, etc. Most Rocket League Mods are written in a popular framework called Bakkesmod (developed Andreas "bakkes" Bakke, a Norwegian software engineer). Rocket League's in-game chat inspired the idea for my submission to NVIDIA's Generative AI Developer Contest: Rocket League BotChat. The idea for my project is to build a plugin with Bakkesmod that allows Rocket League bots to send chat messages based on game events using an LLM accelerated and optimized by TensorRT-LLM (more on TensorRT-LLM soon!)&lt;/p&gt;

&lt;p&gt;Bots are built into the Rocket League game and you can play with or against them in offline matches. However, the built-in bots are not very good. Another 3rd-party project called RLBot allows players to play against community-developed AI bots that are developed with machine learning frameworks like TensorFlow and PyTorch. These bots are very good, but they are not infallible. My contest project idea was now clear: develop a plugin for Rocket League capable of sending messages from bot players. This idea seemed to check the boxes for the large language model category of NVIDIA's developer contest: develop a project in a Windows environment for a Windows-specific program, and use an LLM powered by TensorRT-LLM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4fexamohssy70rel0yfi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4fexamohssy70rel0yfi.png" alt="RLBot" width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Putting together the puzzle pieces&lt;br&gt;
With this idea in mind, I looked into the project's feasibility. I really had no idea if this would work. I looked through the Bakkesmod documentation and found some helpful resources that gave me confidence that I could pull something together for at least a proof-of-concept.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Bakkesmod Plugin Wiki (&lt;a href="https://wiki.bakkesplugins.com/" rel="noopener noreferrer"&gt;https://wiki.bakkesplugins.com/&lt;/a&gt;)

&lt;ul&gt;
&lt;li&gt;HttpWrapper for sending HTTP requests from Bakkesmod&lt;/li&gt;
&lt;li&gt;StatEvents that allow for running custom code when specific event functions are triggered in the game (such as scoring a goal, or making a save).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The Bakkesmod plugin template: &lt;a href="https://github.com/Martinii89/BakkesmodPluginTemplate" rel="noopener noreferrer"&gt;https://github.com/Martinii89/BakkesmodPluginTemplate&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;This provides a great starting-off point for developing Bakkesmod plugins. Plugins for Bakkesmod are written in C++ and this repo provides an organized file structure that allows your to get started quickly&lt;/li&gt;
&lt;li&gt;Plugin Tutorial: &lt;a href="https://wiki.bakkesplugins.com/plugin_tutorial/getting_started/" rel="noopener noreferrer"&gt;https://wiki.bakkesplugins.com/plugin_tutorial/getting_started/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Open-source chat-related Bakkesmod plugins on GitHub

&lt;ul&gt;
&lt;li&gt;BetterChat: &lt;a href="https://github.com/JulienML/BetterChat" rel="noopener noreferrer"&gt;https://github.com/JulienML/BetterChat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Translate: &lt;a href="https://github.com/0xleft/trnslt" rel="noopener noreferrer"&gt;https://github.com/0xleft/trnslt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Starting with the Plugin Template, I wrote a simple console command that when triggered sends an HTTP request to localhost:8000/hello. I set up a Hello World Flask app running on localhost:8000 and I was able to get a response from my Hello World server! There didn't seem to be any network or permission errors that would prevent my game code from communicating with other applications on my PC.&lt;/p&gt;

&lt;p&gt;Next I started looking into how to build and run optimized LLMs with NVIDIA's TensorRT-LLM library, the software that this contest is promoting. The contest announcement included an interesting building block that I thought could be very useful: an example repo showing how to run &lt;code&gt;CodeLlama-13b-instruct-hf&lt;/code&gt; optimized by TensorRT-LLM to provide inference for a VSCode extension called Continue (Continue.dev).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CodeLlama-13b-instruct-hf&lt;/code&gt; is an open source model from Meta that is trained on code and can help with code generation tasks&lt;/li&gt;
&lt;li&gt;TensorRT-LLM is a Python library that accelerates and optimizes inference performance of large language models. It takes a Large Language Model like &lt;code&gt;CodeLlama-13b-instruct-hf&lt;/code&gt; and generates an engine that can be used for doing inference&lt;/li&gt;
&lt;li&gt;VSCode is an open source code editor developed by Microsoft with an large number of community plugins&lt;/li&gt;
&lt;li&gt;Continue.dev is a startup backed by Y Combinator that is developing an open-source autopilot (code assistant) for VSCode and JetBrains that works with local LLMs or paid services like ChatGPT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get the coding assistant project working I needed to build the TensorRT-LLM engine. &lt;/p&gt;

&lt;p&gt;Building TensorRT-LLM engines on Windows can be done in one of two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using a "bare-metal" virtual environment on Windows (with PowerShell)&lt;/li&gt;
&lt;li&gt;using WSL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the time of writing, building a TensorRT-LLM engine on Windows can only be done with version v0.6.1 of the TensorRT-LLM repo and version v0.7.1 of the tensorrt_llm Python package.&lt;/p&gt;

&lt;p&gt;With WSL you can use the up-to-date versions of the TensorRT-LLM repo (main branch). The engines produced by Windows and WSL (Ubuntu) are not interchangeable and you will get errors if you try to use an engine created with one operating system on another operating system.&lt;/p&gt;

&lt;p&gt;Once the engines are built you can use them to run the example from the trt-llm-as-openai-windows repo.&lt;/p&gt;

&lt;p&gt;The example repo exposes an OpenAI-compatible API locally that can do chat completions. You then need to configure the Continue.dev extension to use the local LLM service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CodeLlama-13b-instruct-hf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apiBase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://192.168.5.96:5000/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"None"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Continue.dev extension using CodeLlama-13b-instruct-hf accelerated and optimized by TensorRT-LLM is very fast. According to this post on Continue.dev's blog, C++ is a "first tier" language:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;C++ has one of the largest presences on GitHub and Stack Overflow. This shows up in its representation in public LLM datasets, where it is one of the languages with the most data. Its performance is near the top of the MultiPL-E, BabelCode / TP3, MBXP / Multilingual HumanEval, and HumanEval-X benchmarks. However, given that C++ is often used when code performance and exact algorithm implementation is very important, many developers don’t believe that LLMs are as helpful for C++ as some of the other languages in this tier.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most of the time I'm working with either Python and TypeScript. I've read about C++ but haven't used it for anything before doing this project. I primarily used Microsoft Visual Studio to build the plugin, but VSCode with the Continue.dev autopilot extension was helpful for tackling smaller problems in a REPL-like environment. For example, I used Continue.dev in VSCode to figure out how to handle JSON. Coming from Python and JavaScript languages, I found the nlohmann/json JSON library syntax to be somewhat different. For example, here is how to add a message to messages in the body of an OpenAI API request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push_back&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Python the code for appending a message to a list of messages would be written differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Development environment
&lt;/h2&gt;

&lt;p&gt;While working on different projects using web technologies and frameworks in the Python and JavaScript ecosystems, I developed an appreciation for well-structured development environments that are easy to use. Development environment refers to the tools and processes by which a developer can make a change to source code and see these changes reflected in some version of the application running on a local environment. The local environment (the developer's computer) should be a close proxy for the production environment where the code will ultimately deployed to for end users. For this project the local development environment is our PC itself, which simplifies things. A development environment should support hot-reloading so incremental changes can be run to test functionality, offering a tight feedback loop. I really like the development environment for this project. Here's a screenshot that shows the different parts of the development environment I used for working on Rocket League BotChat:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqh4lwddcnjj91g07qrs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzqh4lwddcnjj91g07qrs.png" alt="Screenshot of Rocket League BotChat development environment" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rocket League (running with the &lt;code&gt;-dev&lt;/code&gt; flag turned on). The console is helpful for viewing log messages and the plugin settings panel can be used to view and change plugin configuration values. The BakkesMod plugin also needs to be running in order to inject plugin code into the game engine&lt;/li&gt;
&lt;li&gt;Visual Studio for working on the plugin code. &lt;code&gt;Control&lt;/code&gt;+&lt;code&gt;Shift&lt;/code&gt;+&lt;code&gt;B&lt;/code&gt; rebuilds the code and automatically reloads the plugin in the game&lt;/li&gt;
&lt;li&gt;OpenAI-compatible LLM server powered by TensorRT-LLM (using &lt;code&gt;Llama-2-13b-chat-hf&lt;/code&gt; with AWQ INT4 quantization) running in a docker container on Ubuntu in WSL&lt;/li&gt;
&lt;li&gt;VSCode for debugging C++ code with Continue.dev extension powered by TensorRT-LLM (using &lt;code&gt;CodeLlama-13b-instruct-hf&lt;/code&gt; with AWQ INT4 quantization) running in a virtual environment on Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building the TensorRT-LLM engines
&lt;/h3&gt;

&lt;p&gt;I was able to build and run the TensorRT LLM engines for my game plugin's inference and the Continue.dev extension's inference both in Python virtual environments on Windows and on Ubuntu in WSL. For building the Llama-2-13b-chat-hf model with INT4 AWQ quantization on Windows 11 I used this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(.venv) PS C:\Users\My PC\GitHub\TensorRT-LLM\examples\llama&amp;gt; python build.py --model_dir D:\llama\Llama-2-13b-chat-hf\ --quant_ckpt_path D:\llama\Llama-2-13b-chat-hf\llama_tp1_rank0.npz --dtype float16 --use_gpt_attention_plugin float16 --use_gemm_plugin float16 --use_weight_only --weight_only_precision int4_awq --per_group --enable_context_fmha --max_batch_size 1 --max_input_len 3500 --max_output_len 1024 --output_dir D:\llama\Llama-2-13b-chat-hf\single-gpu\ --vocab_size 32064
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running the TensorRT-LLM engines
&lt;/h3&gt;

&lt;p&gt;Using Windows PowerShell to start the CodeLlama server for Continue.dev:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(.venv) PS C:\Users\My PC\GitHub\trt-llm-as-openai-windows&amp;gt; python .\app.py --trt_engine_path "D:\llama\CodeLlama-13b-Instruct-hf\trt_engines\1-gpu\" --trt_engine_name llama_float16_tp1_rank0.engine --tokenizer_dir_path "D:\llama\CodeLlama-13b-Instruct-hf\" --port 5000 --host 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tip: Adding &lt;code&gt;--host 0.0.0.0&lt;/code&gt; isn't required here, but it allows me to use the CodeLlama/TensorRT-LLM server with VSCode any computer on my local network using my PC's local IP address in the Continue.dev configuration.&lt;/p&gt;

&lt;p&gt;Using docker in WSL to start the Llama-2-13b-chat-hf LLM server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root@0a5b5b75f079:/code/git/TensorRT-LLM/examples/server/flask# python3 app.py --trt_engine_path /llama/Llama-2-13b-chat-hf/trt_engines/1-gpu/ --trt_engine_name  llama_float16_t_rank0.engine --tokenizer_dir_path /llama/Llama-2-13b-chat-hf/ --port 5001 --host 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: Here I also add &lt;code&gt;--host 0.0.0.0&lt;/code&gt;, but this is required in order for the service in the docker container to be reached from WSL by the game running on Windows.&lt;/p&gt;

&lt;p&gt;BakkesMod includes a console window that came in handy for debugging errors during development.&lt;/p&gt;

&lt;p&gt;At the beginning of this developer contest on January 9, NVIDIA announced Chat with RTX. This is a demo program for Windows that automates a lots of the processes needed to set up a TensorRT-LLM-powered LLM running on your PC. Keep an eye on this project as it may become the best way to install and manage large language models on Windows PCs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwrw1nsmj6a9e7ssuf767.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwrw1nsmj6a9e7ssuf767.jpeg" alt="Chat with RTX" width="800" height="425"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Here's a quick look at key parts of the plugin source code (&lt;a href="https://github.com/briancaffey/RocketLeagueBotChat" rel="noopener noreferrer"&gt;https://github.com/briancaffey/RocketLeagueBotChat&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Hooking events
&lt;/h3&gt;

&lt;p&gt;Hooking events is the core of how this plugin works. StatTickerMessage events cover most of the events that are triggered in Rocket League, such as scoring a goal, making a save or demolishing a car.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Hooks different types of events that are handled in onStatTickerMessage&lt;/span&gt;
    &lt;span class="c1"&gt;// See https://wiki.bakkesplugins.com/functions/stat_events/&lt;/span&gt;
    &lt;span class="n"&gt;gameWrapper&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;HookEventWithCallerPost&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ServerWrapper&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Function TAGame.GFxHUD_TA.HandleStatTickerMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;ServerWrapper&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;eventname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;onStatTickerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Handling events and building the prompt
&lt;/h3&gt;

&lt;p&gt;We can unpack values from the event to determine the player to which the event should be attributed. The code then translates the game event and related data into an English sentence. This is appended to a vector of message objects with the appendToPrompt method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// handle different events like scoring a goal or making a save&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetEventName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Goal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// was the goal scored by the human player or the bot?&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerPRI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_address&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;receiver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;appendToPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Your human opponent just scored a goal against you! "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;score_sentence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;appendToPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"You just scored a goal against the human player! "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;score_sentence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Making requests and handling responses
&lt;/h3&gt;

&lt;p&gt;The last main part of the code is making a request to the LLM server with the prompt that we have formed above based on game messages. This code should look familiar to anyone who has worked with OpenAI's API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response_json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"choices"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;LogToChatbox&lt;/code&gt; method is used to send a message to the in-game chat box with the name of the bot that is sending the message. Since messages could possibly be longer than the limit of 120 characters, I send messages to the chatbox in chunks of 120 characters at a time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;gameWrapper&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;LogToChatbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bot_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! The code isn't that complicated. I had to sanitize the message so that it would not include emoji or the stop character that the LLM server would include in messages (&lt;code&gt;&amp;lt;/s&amp;gt;&lt;/code&gt;). Oddly, I had a hard time getting the LLM to not use emoji even when I instructed it to not use emoji in the system prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rocket League BotChat UI
&lt;/h2&gt;

&lt;p&gt;Most BakkesMod plugins for RocketLeague UIs that allow for controlling settings. Here's what the UI for Rocket League BotChat looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnsbm57h6dnxf1jo877d8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnsbm57h6dnxf1jo877d8.png" alt="Rocket League BotChat Plugin UI" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  System prompt
&lt;/h3&gt;

&lt;p&gt;The system prompt instructs the bot on how it shoud reply. This is an important part of the prompt engineering for this project, and I used Postman to experiment with lots of different types of instructions. Here's the default prompt that I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ai_player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"You are an elite AI player in the car soccer game Rocket League. "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;one_v_one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"You are playing a 1v1 match against a human player. "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"You will send short chat messages to your human opponent in response to what happens in the game. "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Respond to the human player with brief messages no more than 12 words long."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// initial system prompt&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;initial_system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ai_player&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;one_v_one&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last part about no more than 12 words long was the most effective way of controlling the length responses from the LLM. I tried changing the max_output_len when building the TensorRT engine, but this degraded the quality of the responses. The system prompt can be changed by the user. Changing the system prompt was a lot of fun to expirment with!&lt;/p&gt;

&lt;h3&gt;
  
  
  Temperature and Seed
&lt;/h3&gt;

&lt;p&gt;These values are included in the body of the request to the LLM, but I didn't have much luck with these. Early on I had issues with getting sufficient variation in the responses from the LLM, so I tried using random values for seed and temperature, but this didn't really work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Messages
&lt;/h3&gt;

&lt;p&gt;This section of the UI displays the messages that are used in requests to the LLM. In order keep the prompt within the context window limit, I only used the most recent six messages sent from the "user" (which are messages about game events) and the "assistant" (which are LLM responses from the bot). Whenever the user changes the system prompt, the messages vector is reset to only include the new system prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Video for Contest Submission
&lt;/h2&gt;

&lt;p&gt;I used Blender's sequence editor to create a demo video for my contest submission. I don't edit a lot of videos, but it is a fun process and I learned a lot about Blender and non-linear video editing in the process. Here's how I approached creating the demo video for my project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fblender.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fblender.png" alt="Blender video sequence editor UI used to create my project video" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Structure the video in three main parts: introduction to my project and the contest, description of how it works, demo of my project in action&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Find an upbeat song from playlists included in Rocket League with no vocals to use as background music. I used "Dads in Space" by Steven Walking&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Get stock Rocket League footage from YouTube with youtube-dl (this is an amazing tool!). I mostly used footage from the RLCS 2023 Winter Major Trailer. This video was uploaded at 24 fps, and my Blender Video project frame rate was set to 29.97, so I used ffmpeg to convert this video from 24 fps to 29.97 fps.&lt;br&gt;
Record myself playing Rocket League with my plugin enabled using NVIDIA Share. Miraculously, I was able to score against the Nexto bot!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use ComfyUI to animate some of the images used in the contest description and use these in my video&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fcomfyui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fcomfyui.png" alt="ComfyUI workflow for animating images using img2vid model" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use ElevenLabs to narrate a simple voice over script that describes the video content. This tuned out a lot better than I expected. I paid $1 for the ElevenLabs creator plan and got lots of tokens to experiment with different settings for voice generation using a clone of my voice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Felevenlabs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Felevenlabs.png" alt="Eleven Labs Voice Generation Web UI" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Shortcomings of my project
&lt;/h2&gt;

&lt;p&gt;This plugin is a proof of concept and it has some shortcomings. One issue is that some events that my plugin listens to can happen in rapid succession. This results in "user" and "assistant" prompts getting out of order which breaks assertions on the trt-llm-as-openai-windows repo. It would make more sense to have the bot send messages not immediately after the events are triggered, but on a different type of schedule that allows for multiple events to happen before sending the prompt to the LLM.&lt;/p&gt;

&lt;p&gt;There are lots of events that are triggered that would be interesting things for the bot to react to, but I decided not to prompt on every event since the above situation would be triggered frequently. For example, suppose I listen for events like taking a shot on goal and scoring a goal. If the goal is scored immediately after the shot is taken, then the second prompt is sent before the response for the first prompt comes back. For this reason I decided to simply not listen to events like "shot on goal" to avoid prompt messages getting out of order. This could also be addressed with more code logic.&lt;/p&gt;

&lt;p&gt;Prompt engineering is something that can always be improved. It is hard to measure and testing it is subjective. I am pleased with the results I was able to capture for the demo video, but the quality of the LLM responses can very depending on what happens during gameplay. One idea I had to address this would be to provide multiple English translations for any given event, and then select one at random. This might help improve the variety of responses, for example.&lt;/p&gt;

&lt;p&gt;I faced some limitations that are built in to the game iteself. For example, it is not possible for a player to send messages to the in-game chat in offline matches, which makes sense! I built a backdoor for doing this through the BakkesMod developer console, so you can send messages to the bot by typing something like SendMessage Good shot, bot!, for example.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;Participating in this contest was a great opportunity to learn more about LLMs and how to use them to extend programs in a Windows environment. It was also a lot of fun to build something by putting together new tools like TensorRT-LLM. Seeing the bot send me chat messages was very satisfying when I first got it to work! Overall it is a pretty simple implementation, but this idea could be extended to produce useful application. I could imagine a "Rocket League Coach" plugin that expands on this idea to give helpful feedback based on higher-level data, statistical trends, training goals, etc.&lt;/p&gt;

&lt;p&gt;I think the gaming industry's adoption of LLMs for new games will be BIG, and it will present a huge opportunity for LLM optimization and acceleration software like TensorRT-LLM that I was able to use in my Rocket League BotChat. This is not to discredit the work of writers which play an important role in game development. I'm excited to see what other developers have built for this contest, especially submissions that are building mods for games using TensorRT-LLM.&lt;/p&gt;

&lt;p&gt;Thanks NVIDIA and the TensorRT and TensorRT-LLM teams for organizing this contest! Keep on building!!&lt;/p&gt;

</description>
      <category>nvidia</category>
      <category>rtx</category>
      <category>llm</category>
      <category>rocketleague</category>
    </item>
    <item>
      <title>Rocket League BotChat powered by TensorRT-LLM: My submission for NVIDIA's Generative AI on RTX PCs Developer Contest</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Mon, 19 Feb 2024 01:50:22 +0000</pubDate>
      <link>https://dev.to/briancaffey/rocket-league-botchat-powered-by-tensorrt-llm-my-submission-for-nvidias-generative-ai-on-rtx-pcs-developer-contest-1bkb</link>
      <guid>https://dev.to/briancaffey/rocket-league-botchat-powered-by-tensorrt-llm-my-submission-for-nvidias-generative-ai-on-rtx-pcs-developer-contest-1bkb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fcover.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fcover.png" alt="Rocket League BotChat" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;This article is about my submission to NVIDIA's Generative AI on RTX PCs Developer Contest: Rocket League BotChat. Rocket League BotChat is a BakkesMod plugin for Rocket League that allows bots to send chat messages based on in-game events. It is designed to be used with a local LLM service optimized and accelerated with NVIDIA's TensorRT-LLM library.&lt;/p&gt;

&lt;p&gt;Here's my project submission post on 𝕏:&lt;/p&gt;

&lt;p&gt;&lt;iframe class="tweet-embed" id="tweet-1759265152275292644-805" src="https://platform.twitter.com/embed/Tweet.html?id=1759265152275292644"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1759265152275292644-805');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1759265152275292644&amp;amp;theme=dark"
  }



&lt;/p&gt;

&lt;p&gt;Here's a link to the Rocket League BotChat GitHub repository.&lt;/p&gt;

&lt;p&gt;NVIDIA's Gen AI Developer Contest&lt;br&gt;
The following email caught my attention last month:&lt;/p&gt;

&lt;p&gt;Generative AI on RTX PCs Developer Contest: Build your next innovative Gen AI project using NVIDIA TensorRT or TensorRT-LLM on Windows PC with NVIDIA RTX systems&lt;/p&gt;

&lt;p&gt;The part about “on Windows PC” made me think: why would a developer contest focus on a particular operating system? I use all three of the major operating systems: macOS, Ubuntu and Windows 11, but most of the development work I do is on macOS and Ubuntu. I discovered WSL (Windows Subsystem for Linux) a few years ago and really enjoy using that for development as well, but I had never considered doing development work on Windows outside of WSL. I had also never used any of the Windows-specific development frameworks like .NET or Visual Studio.&lt;/p&gt;

&lt;p&gt;My experience with Windows goes back to 2016 when I built my fist PC with an NVIDIA GeForce GTX 1080 graphics card. When I built another personal computer last year in 2023, getting the NVIDIA GeForce RTX 4090 graphics card was a big step up. I bought two NVMe drives in order to dual boot into both Windows and Ubuntu operating systems. Switching between the operating systems requires turning off the computer, going into the BIOS settings and changing the boot order and restarting the computer.&lt;/p&gt;

&lt;p&gt;Last year I started learning more about AI image generation using Stable Diffusion with programs like Automatic1111, InvokeAI and ComfyUI. I set up everything on my PC's Ubuntu operating system, and frequently had to switch between using Ubuntu for working with stable diffusion and Windows for gaming and other Windows-specific software. The friction of having to constantly switch operating systems pushed me to move my stable diffusion software workflows to Windows. All of my models and images are stored to external drives, so moving things over to Windows was pretty easy.&lt;/p&gt;

&lt;p&gt;I learned PowerShell and got more familiar with how Windows works as a development machine. Environment variables and system variables are one example of how Windows does things differently compared ot Linux-based operating systems. And just like that, I became a Windows developer! This experience got me interested in coming up with an idea for the NVIDIA Generative AI on NVIDIA RTX PCs Developer Contest.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fwinfetch.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fwinfetch.png" alt="winfetch" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Coming up with an Idea&lt;br&gt;
The contest description and some related NVIDIA articles about the contest helped me with brainstorming:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Whether it’s a RAG-based chatbot, a plug-in for an existing application, or a code generation tool, the possibilities are endless.&lt;/p&gt;

&lt;p&gt;Many use cases would benefit from running LLMs locally on Windows PCs, including gaming, creativity, productivity, and developer experiences.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This contest is focused on NVIDIA's consumer hardware line: GeForce RTX. It has a diverse set of use cases including gaming, crypto mining, VR, simulation software, creative tools and new AI techniques including image generation and LLM (Large Language Model) inference.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fg.foolcdn.com%2Fimage%2F%3Furl%3Dhttps%253A%252F%252Fg.foolcdn.com%252Feditorial%252Fimages%252F764886%252Fnvda_revenue_bar.png%26op%3Dresize%26w%3D700" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fg.foolcdn.com%2Fimage%2F%3Furl%3Dhttps%253A%252F%252Fg.foolcdn.com%252Feditorial%252Fimages%252F764886%252Fnvda_revenue_bar.png%26op%3Dresize%26w%3D700" alt="A stacked bar chart showing the composition of Nvidia's revenue each quarter going back to fiscal 2019" width="700" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Gaming seemed like an interesting avenue for me to explore. PC gaming is still an industry that is developed primarily for Windows operating systems, and the gaming industry has been the largest revenue driver of NVIDIA in recent years, only recently surpassed by the data center segment. GPUs are needed to render graphics of enormous open-world environments. Some story-driven games include huge amounts of dialogue that can be considered as huge literary works in their own right. Red Dead Redemption and Genshin Impact are two massively popular games of this type. There might be an interesting project idea that could use LLMs and RAG (retrieval augmented generation), but I don't play these types of games and it didn't seem practical for a project that would be built in just over a month. I thought about trying to build something for a simpler game that I already know.&lt;/p&gt;

&lt;p&gt;Rocket League is a vehicular soccer game that is played on both game consoles and on PCs. It is an eSports with a very high skill ceiling and a massive player base (85 million active players in the last 30 days). I started playing it during the pandemic with some of my friends and all got hooked. We also came to learn that Rocket League's in-game is varies from entertaining, annoying, toxic and in some cases, sportsmanlike.&lt;/p&gt;

&lt;p&gt;One other thing I learned about Rocket League is that it has an active modding community. Developers create plugins for the game for all different purposes, such as coaching, practice drills, capturing replays, tracking player statistics, etc. Most Rocket League Mods are written in a popular framework called Bakkesmod (developed Andreas "bakkes" Bakke, a Norwegian software engineer). Rocket League's in-game chat inspired the idea for my submission to NVIDIA's Generative AI Developer Contest: Rocket League BotChat. The idea for my project is to build a plugin with Bakkesmod that allows Rocket League bots to send chat messages based on game events using an LLM accelerated and optimized by TensorRT-LLM (more on TensorRT-LLM soon!)&lt;/p&gt;

&lt;p&gt;Bots are built into the Rocket League game and you can play with or against them in offline matches. However, the built-in bots are not very good. Another 3rd-party project called RLBot allows players to play against community-developed AI bots that are developed with machine learning frameworks like TensorFlow and PyTorch. These bots are very good, but they are not infallible. My contest project idea was now clear: develop a plugin for Rocket League capable of sending messages from bot players. This idea seemed to check the boxes for the large language model category of NVIDIA's developer contest: develop a project in a Windows environment for a Windows-specific program, and use an LLM powered by TensorRT-LLM.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fbot.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fbot.png" alt="RLBot" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Putting together the puzzle pieces&lt;br&gt;
With this idea in mind, I looked into the project's feasibility. I really had no idea if this would work. I looked through the Bakkesmod documentation and found some helpful resources that gave me confidence that I could pull something together for at least a proof-of-concept.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Bakkesmod Plugin Wiki (&lt;a href="https://wiki.bakkesplugins.com/" rel="noopener noreferrer"&gt;https://wiki.bakkesplugins.com/&lt;/a&gt;)

&lt;ul&gt;
&lt;li&gt;HttpWrapper for sending HTTP requests from Bakkesmod&lt;/li&gt;
&lt;li&gt;StatEvents that allow for running custom code when specific event functions are triggered in the game (such as scoring a goal, or making a save).&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The Bakkesmod plugin template: &lt;a href="https://github.com/Martinii89/BakkesmodPluginTemplate" rel="noopener noreferrer"&gt;https://github.com/Martinii89/BakkesmodPluginTemplate&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;This provides a great starting-off point for developing Bakkesmod plugins. Plugins for Bakkesmod are written in C++ and this repo provides an organized file structure that allows your to get started quickly&lt;/li&gt;
&lt;li&gt;Plugin Tutorial: &lt;a href="https://wiki.bakkesplugins.com/plugin_tutorial/getting_started/" rel="noopener noreferrer"&gt;https://wiki.bakkesplugins.com/plugin_tutorial/getting_started/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Open-source chat-related Bakkesmod plugins on GitHub

&lt;ul&gt;
&lt;li&gt;BetterChat: &lt;a href="https://github.com/JulienML/BetterChat" rel="noopener noreferrer"&gt;https://github.com/JulienML/BetterChat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Translate: &lt;a href="https://github.com/0xleft/trnslt" rel="noopener noreferrer"&gt;https://github.com/0xleft/trnslt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Starting with the Plugin Template, I wrote a simple console command that when triggered sends an HTTP request to localhost:8000/hello. I set up a Hello World Flask app running on localhost:8000 and I was able to get a response from my Hello World server! There didn't seem to be any network or permission errors that would prevent my game code from communicating with other applications on my PC.&lt;/p&gt;

&lt;p&gt;Next I started looking into how to build and run optimized LLMs with NVIDIA's TensorRT-LLM library, the software that this contest is promoting. The contest announcement included an interesting building block that I thought could be very useful: an example repo showing how to run &lt;code&gt;CodeLlama-13b-instruct-hf&lt;/code&gt; optimized by TensorRT-LLM to provide inference for a VSCode extension called Continue (Continue.dev).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CodeLlama-13b-instruct-hf&lt;/code&gt; is an open source model from Meta that is trained on code and can help with code generation tasks&lt;/li&gt;
&lt;li&gt;TensorRT-LLM is a Python library that accelerates and optimizes inference performance of large language models. It takes a Large Language Model like &lt;code&gt;CodeLlama-13b-instruct-hf&lt;/code&gt; and generates an engine that can be used for doing inference&lt;/li&gt;
&lt;li&gt;VSCode is an open source code editor developed by Microsoft with an large number of community plugins&lt;/li&gt;
&lt;li&gt;Continue.dev is a startup backed by Y Combinator that is developing an open-source autopilot (code assistant) for VSCode and JetBrains that works with local LLMs or paid services like ChatGPT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get the coding assistant project working I needed to build the TensorRT-LLM engine. &lt;/p&gt;

&lt;p&gt;Building TensorRT-LLM engines on Windows can be done in one of two ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using a "bare-metal" virtual environment on Windows (with PowerShell)&lt;/li&gt;
&lt;li&gt;using WSL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the time of writing, building a TensorRT-LLM engine on Windows can only be done with version v0.6.1 of the TensorRT-LLM repo and version v0.7.1 of the tensorrt_llm Python package.&lt;/p&gt;

&lt;p&gt;With WSL you can use the up-to-date versions of the TensorRT-LLM repo (main branch). The engines produced by Windows and WSL (Ubuntu) are not interchangeable and you will get errors if you try to use an engine created with one operating system on another operating system.&lt;/p&gt;

&lt;p&gt;Once the engines are built you can use them to run the example from the trt-llm-as-openai-windows repo.&lt;/p&gt;

&lt;p&gt;The example repo exposes an OpenAI-compatible API locally that can do chat completions. You then need to configure the Continue.dev extension to use the local LLM service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CodeLlama-13b-instruct-hf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apiBase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://192.168.5.96:5000/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"openai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"apiKey"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"None"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gpt-4"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Continue.dev extension using CodeLlama-13b-instruct-hf accelerated and optimized by TensorRT-LLM is very fast. According to this post on Continue.dev's blog, C++ is a "first tier" language:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;C++ has one of the largest presences on GitHub and Stack Overflow. This shows up in its representation in public LLM datasets, where it is one of the languages with the most data. Its performance is near the top of the MultiPL-E, BabelCode / TP3, MBXP / Multilingual HumanEval, and HumanEval-X benchmarks. However, given that C++ is often used when code performance and exact algorithm implementation is very important, many developers don’t believe that LLMs are as helpful for C++ as some of the other languages in this tier.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Most of the time I'm working with either Python and TypeScript. I've read about C++ but haven't used it for anything before doing this project. I primarily used Microsoft Visual Studio to build the plugin, but VSCode with the Continue.dev autopilot extension was helpful for tackling smaller problems in a REPL-like environment. For example, I used Continue.dev in VSCode to figure out how to handle JSON. Coming from Python and JavaScript languages, I found the nlohmann/json JSON library syntax to be somewhat different. For example, here is how to add a message to messages in the body of an OpenAI API request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push_back&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Python the code for appending a message to a list of messages would be written differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;role&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Development environment
&lt;/h2&gt;

&lt;p&gt;While working on different projects using web technologies and frameworks in the Python and JavaScript ecosystems, I developed an appreciation for well-structured development environments that are easy to use. Development environment refers to the tools and processes by which a developer can make a change to source code and see these changes reflected in some version of the application running on a local environment. The local environment (the developer's computer) should be a close proxy for the production environment where the code will ultimately deployed to for end users. For this project the local development environment is our PC itself, which simplifies things. A development environment should support hot-reloading so incremental changes can be run to test functionality, offering a tight feedback loop. I really like the development environment for this project. Here's a screenshot that shows the different parts of the development environment I used for working on Rocket League BotChat:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fdevenv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fdevenv2.png" alt="Screenshot of Rocket League BotChat development environment" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rocket League (running with the &lt;code&gt;-dev&lt;/code&gt; flag turned on). The console is helpful for viewing log messages and the plugin settings panel can be used to view and change plugin configuration values. The BakkesMod plugin also needs to be running in order to inject plugin code into the game engine&lt;/li&gt;
&lt;li&gt;Visual Studio for working on the plugin code. &lt;code&gt;Control&lt;/code&gt;+&lt;code&gt;Shift&lt;/code&gt;+&lt;code&gt;B&lt;/code&gt; rebuilds the code and automatically reloads the plugin in the game&lt;/li&gt;
&lt;li&gt;OpenAI-compatible LLM server powered by TensorRT-LLM (using &lt;code&gt;Llama-2-13b-chat-hf&lt;/code&gt; with AWQ INT4 quantization) running in a docker container on Ubuntu in WSL&lt;/li&gt;
&lt;li&gt;VSCode for debugging C++ code with Continue.dev extension powered by TensorRT-LLM (using &lt;code&gt;CodeLlama-13b-instruct-hf&lt;/code&gt; with AWQ INT4 quantization) running in a virtual environment on Windows&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Building the TensorRT-LLM engines
&lt;/h3&gt;

&lt;p&gt;I was able to build and run the TensorRT LLM engines for my game plugin's inference and the Continue.dev extension's inference both in Python virtual environments on Windows and on Ubuntu in WSL. For building the Llama-2-13b-chat-hf model with INT4 AWQ quantization on Windows 11 I used this command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(.venv) PS C:\Users\My PC\GitHub\TensorRT-LLM\examples\llama&amp;gt; python build.py --model_dir D:\llama\Llama-2-13b-chat-hf\ --quant_ckpt_path D:\llama\Llama-2-13b-chat-hf\llama_tp1_rank0.npz --dtype float16 --use_gpt_attention_plugin float16 --use_gemm_plugin float16 --use_weight_only --weight_only_precision int4_awq --per_group --enable_context_fmha --max_batch_size 1 --max_input_len 3500 --max_output_len 1024 --output_dir D:\llama\Llama-2-13b-chat-hf\single-gpu\ --vocab_size 32064
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running the TensorRT-LLM engines
&lt;/h3&gt;

&lt;p&gt;Using Windows PowerShell to start the CodeLlama server for Continue.dev:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(.venv) PS C:\Users\My PC\GitHub\trt-llm-as-openai-windows&amp;gt; python .\app.py --trt_engine_path "D:\llama\CodeLlama-13b-Instruct-hf\trt_engines\1-gpu\" --trt_engine_name llama_float16_tp1_rank0.engine --tokenizer_dir_path "D:\llama\CodeLlama-13b-Instruct-hf\" --port 5000 --host 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tip: Adding &lt;code&gt;--host 0.0.0.0&lt;/code&gt; isn't required here, but it allows me to use the CodeLlama/TensorRT-LLM server with VSCode any computer on my local network using my PC's local IP address in the Continue.dev configuration.&lt;/p&gt;

&lt;p&gt;Using docker in WSL to start the Llama-2-13b-chat-hf LLM server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root@0a5b5b75f079:/code/git/TensorRT-LLM/examples/server/flask# python3 app.py --trt_engine_path /llama/Llama-2-13b-chat-hf/trt_engines/1-gpu/ --trt_engine_name  llama_float16_t_rank0.engine --tokenizer_dir_path /llama/Llama-2-13b-chat-hf/ --port 5001 --host 0.0.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note: Here I also add &lt;code&gt;--host 0.0.0.0&lt;/code&gt;, but this is required in order for the service in the docker container to be reached from WSL by the game running on Windows.&lt;/p&gt;

&lt;p&gt;BakkesMod includes a console window that came in handy for debugging errors during development.&lt;/p&gt;

&lt;p&gt;At the beginning of this developer contest on January 9, NVIDIA announced Chat with RTX. This is a demo program for Windows that automates a lots of the processes needed to set up a TensorRT-LLM-powered LLM running on your PC. Keep an eye on this project as it may become the best way to install and manage large language models on Windows PCs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fchat_with_rtx.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fchat_with_rtx.jpeg" alt="Chat with RTX" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Here's a quick look at key parts of the plugin source code (&lt;a href="https://github.com/briancaffey/RocketLeagueBotChat" rel="noopener noreferrer"&gt;https://github.com/briancaffey/RocketLeagueBotChat&lt;/a&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Hooking events
&lt;/h3&gt;

&lt;p&gt;Hooking events is the core of how this plugin works. StatTickerMessage events cover most of the events that are triggered in Rocket League, such as scoring a goal, making a save or demolishing a car.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// Hooks different types of events that are handled in onStatTickerMessage&lt;/span&gt;
    &lt;span class="c1"&gt;// See https://wiki.bakkesplugins.com/functions/stat_events/&lt;/span&gt;
    &lt;span class="n"&gt;gameWrapper&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;HookEventWithCallerPost&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ServerWrapper&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Function TAGame.GFxHUD_TA.HandleStatTickerMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;ServerWrapper&lt;/span&gt; &lt;span class="n"&gt;caller&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;eventname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;onStatTickerMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Handling events and building the prompt
&lt;/h3&gt;

&lt;p&gt;We can unpack values from the event to determine the player to which the event should be attributed. The code then translates the game event and related data into an English sentence. This is appended to a vector of message objects with the appendToPrompt method.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// handle different events like scoring a goal or making a save&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetEventName&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s"&gt;"Goal"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// was the goal scored by the human player or the bot?&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;playerPRI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_address&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;receiver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_address&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;appendToPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Your human opponent just scored a goal against you! "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;score_sentence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;appendToPrompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"You just scored a goal against the human player! "&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;score_sentence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Making requests and handling responses
&lt;/h3&gt;

&lt;p&gt;The last main part of the code is making a request to the LLM server with the prompt that we have formed above based on game messages. This code should look familiar to anyone who has worked with OpenAI's API.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response_json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"choices"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;LogToChatbox&lt;/code&gt; method is used to send a message to the in-game chat box with the name of the bot that is sending the message. Since messages could possibly be longer than the limit of 120 characters, I send messages to the chatbox in chunks of 120 characters at a time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;gameWrapper&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;LogToChatbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bot_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it! The code isn't that complicated. I had to sanitize the message so that it would not include emoji or the stop character that the LLM server would include in messages (&lt;code&gt;&amp;lt;/s&amp;gt;&lt;/code&gt;). Oddly, I had a hard time getting the LLM to not use emoji even when I instructed it to not use emoji in the system prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rocket League BotChat UI
&lt;/h2&gt;

&lt;p&gt;Most BakkesMod plugins for RocketLeague UIs that allow for controlling settings. Here's what the UI for Rocket League BotChat looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Frlbcui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Frlbcui.png" alt="Rocket League BotChat Plugin UI" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  System prompt
&lt;/h3&gt;

&lt;p&gt;The system prompt instructs the bot on how it shoud reply. This is an important part of the prompt engineering for this project, and I used Postman to experiment with lots of different types of instructions. Here's the default prompt that I used:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ai_player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"You are an elite AI player in the car soccer game Rocket League. "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;one_v_one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"You are playing a 1v1 match against a human player. "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"You will send short chat messages to your human opponent in response to what happens in the game. "&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Respond to the human player with brief messages no more than 12 words long."&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// initial system prompt&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="n"&gt;initial_system_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ai_player&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;one_v_one&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;instructions&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;details&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last part about no more than 12 words long was the most effective way of controlling the length responses from the LLM. I tried changing the max_output_len when building the TensorRT engine, but this degraded the quality of the responses. The system prompt can be changed by the user. Changing the system prompt was a lot of fun to expirment with!&lt;/p&gt;

&lt;h3&gt;
  
  
  Temperature and Seed
&lt;/h3&gt;

&lt;p&gt;These values are included in the body of the request to the LLM, but I didn't have much luck with these. Early on I had issues with getting sufficient variation in the responses from the LLM, so I tried using random values for seed and temperature, but this didn't really work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Messages
&lt;/h3&gt;

&lt;p&gt;This section of the UI displays the messages that are used in requests to the LLM. In order keep the prompt within the context window limit, I only used the most recent six messages sent from the "user" (which are messages about game events) and the "assistant" (which are LLM responses from the bot). Whenever the user changes the system prompt, the messages vector is reset to only include the new system prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo Video for Contest Submission
&lt;/h2&gt;

&lt;p&gt;I used Blender's sequence editor to create a demo video for my contest submission. I don't edit a lot of videos, but it is a fun process and I learned a lot about Blender and non-linear video editing in the process. Here's how I approached creating the demo video for my project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fblender.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fblender.png" alt="Blender video sequence editor UI used to create my project video" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Structure the video in three main parts: introduction to my project and the contest, description of how it works, demo of my project in action&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Find an upbeat song from playlists included in Rocket League with no vocals to use as background music. I used "Dads in Space" by Steven Walking&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Get stock Rocket League footage from YouTube with youtube-dl (this is an amazing tool!). I mostly used footage from the RLCS 2023 Winter Major Trailer. This video was uploaded at 24 fps, and my Blender Video project frame rate was set to 29.97, so I used ffmpeg to convert this video from 24 fps to 29.97 fps.&lt;br&gt;
Record myself playing Rocket League with my plugin enabled using NVIDIA Share. Miraculously, I was able to score against the Nexto bot!&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use ComfyUI to animate some of the images used in the contest description and use these in my video&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fcomfyui.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Fcomfyui.png" alt="ComfyUI workflow for animating images using img2vid model" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use ElevenLabs to narrate a simple voice over script that describes the video content. This tuned out a lot better than I expected. I paid $1 for the ElevenLabs creator plan and got lots of tokens to experiment with different settings for voice generation using a clone of my voice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Felevenlabs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fbriancaffey.github.io%2Fimg%2Frlbc%2Felevenlabs.png" alt="Eleven Labs Voice Generation Web UI" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Shortcomings of my project
&lt;/h2&gt;

&lt;p&gt;This plugin is a proof of concept and it has some shortcomings. One issue is that some events that my plugin listens to can happen in rapid succession. This results in "user" and "assistant" prompts getting out of order which breaks assertions on the trt-llm-as-openai-windows repo. It would make more sense to have the bot send messages not immediately after the events are triggered, but on a different type of schedule that allows for multiple events to happen before sending the prompt to the LLM.&lt;/p&gt;

&lt;p&gt;There are lots of events that are triggered that would be interesting things for the bot to react to, but I decided not to prompt on every event since the above situation would be triggered frequently. For example, suppose I listen for events like taking a shot on goal and scoring a goal. If the goal is scored immediately after the shot is taken, then the second prompt is sent before the response for the first prompt comes back. For this reason I decided to simply not listen to events like "shot on goal" to avoid prompt messages getting out of order. This could also be addressed with more code logic.&lt;/p&gt;

&lt;p&gt;Prompt engineering is something that can always be improved. It is hard to measure and testing it is subjective. I am pleased with the results I was able to capture for the demo video, but the quality of the LLM responses can very depending on what happens during gameplay. One idea I had to address this would be to provide multiple English translations for any given event, and then select one at random. This might help improve the variety of responses, for example.&lt;/p&gt;

&lt;p&gt;I faced some limitations that are built in to the game iteself. For example, it is not possible for a player to send messages to the in-game chat in offline matches, which makes sense! I built a backdoor for doing this through the BakkesMod developer console, so you can send messages to the bot by typing something like SendMessage Good shot, bot!, for example.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;Participating in this contest was a great opportunity to learn more about LLMs and how to use them to extend programs in a Windows environment. It was also a lot of fun to build something by putting together new tools like TensorRT-LLM. Seeing the bot send me chat messages was very satisfying when I first got it to work! Overall it is a pretty simple implementation, but this idea could be extended to produce useful application. I could imagine a "Rocket League Coach" plugin that expands on this idea to give helpful feedback based on higher-level data, statistical trends, training goals, etc.&lt;/p&gt;

&lt;p&gt;I think the gaming industry's adoption of LLMs for new games will be BIG, and it will present a huge opportunity for LLM optimization and acceleration software like TensorRT-LLM that I was able to use in my Rocket League BotChat. This is not to discredit the work of writers which play an important role in game development. I'm excited to see what other developers have built for this contest, especially submissions that are building mods for games using TensorRT-LLM.&lt;/p&gt;

&lt;p&gt;Thanks NVIDIA and the TensorRT and TensorRT-LLM teams for organizing this contest! Keep on building!!&lt;/p&gt;

</description>
      <category>nvidia</category>
      <category>rtx</category>
      <category>llm</category>
      <category>rocketleague</category>
    </item>
    <item>
      <title>My Infrastructure as Code Rosetta Stone - Deploying the same web application on AWS ECS Fargate with CDK, Terraform and Pulumi</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Sat, 07 Jan 2023 18:54:38 +0000</pubDate>
      <link>https://dev.to/briancaffey/my-infrastructure-as-code-rosetta-stone-deploying-the-same-web-application-on-aws-ecs-fargate-with-cdk-terraform-and-pulumi-oe4</link>
      <guid>https://dev.to/briancaffey/my-infrastructure-as-code-rosetta-stone-deploying-the-same-web-application-on-aws-ecs-fargate-with-cdk-terraform-and-pulumi-oe4</guid>
      <description>&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;I wrote three infrastructure as code libraries for deploying containerized 3-tier web apps on AWS ECS Fargate using CDK, Terraform and Pulumi. This article will provide an overview of my experience working with these three IaC tools and will show how I use my libraries in automated infrastructure deployment pipelines with GitHub Actions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CDK Construct Library&lt;/strong&gt;: &lt;a href="https://github.com/briancaffey/cdk-django" rel="noopener noreferrer"&gt;github.com/briancaffey/cdk-django&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform Modules&lt;/strong&gt;: &lt;a href="https://github.com/briancaffey/terraform-aws-django" rel="noopener noreferrer"&gt;github.com/briancaffey/terraform-aws-django&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pulumi Component Library&lt;/strong&gt;: &lt;a href="https://github.com/briancaffey/pulumi-aws-django" rel="noopener noreferrer"&gt;github.com/briancaffey/pulumi-aws-django&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Mono repo with a sample Django micro blogging app (μblog) and frontend app (Vue SPA written with Quasar), GitHub Action workflows for infrastructure and (separate) application deployment pipelines, IaC code that &lt;em&gt;consumes&lt;/em&gt; each of the libraries listed above, &lt;a href="https://briancaffey.github.io/django-step-by-step/" rel="noopener noreferrer"&gt;VuePress documentation site&lt;/a&gt; and miscellaneous items (k6 load testing scripts, Cypress tests, docker-compose, etc.): &lt;a href="https://github.com/briancaffey/django-step-by-step" rel="noopener noreferrer"&gt;github.com/briancaffey/django-step-by-step&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  eli5
&lt;/h2&gt;

&lt;p&gt;Pretend we are at the beach building sandcastles. We can build sandcastles using our hands, but this takes a lot of time, and we might bump into each other and accidentally knock over part of our sandcastle. I made some tools for building sandcastles. We have one tool for building a sand castle base that includes the wall around the outside, the moat, the door and different sections inside the walls. And I made another tool for deploying smaller sand castle houses inside the walls of the sandcastle base. We fill the tool with sand and water and then turn it over inside of our base and we can build an entire city of sandcastles. Also, the tool lets us carefully remove parts of our sandcastle without knocking over any of the other parts. We can share the tool with all of our friends and they can make cool sandcastles too, and the tool is free for them to use.&lt;/p&gt;

&lt;p&gt;Instead of sandcastles, I'm working with computer systems that can power internet applications, like YouTube for example. I'm building tools that can allow me or anyone else to build really awesome internet applications using computers.&lt;/p&gt;

&lt;p&gt;The tools are not physical tools like the ones for building sandcastles, but instead these tools are made with code. The code for websites like YouTube allow you upload videos &lt;em&gt;to YouTube&lt;/em&gt;, but the code I'm writing allows you to upload any type of website (even on like YouTube) &lt;em&gt;to the internet&lt;/em&gt;. When we run this code, it creates applications on the internet. Also, sand is very expensive and Jeff Bezos owns the beach.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made an Infrastructure as Code Rosetta Stone with CDK, Terraform and Pulumi
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb4914r9255d0tnb1gch6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb4914r9255d0tnb1gch6.png" alt="IaC Rosetta Stone" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To push myself to learn more about AWS, IaC, CI/CD, automation and Platform Engineering&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Learn differences between major IaC tools and how to use them to do exactly the same thing (build a web app) on the same Cloud (AWS) in the same way (serverless container technology using ECS Fargate).&lt;/li&gt;
&lt;li&gt;Get more experience publishing software packages (npm) and finding the right level of abstraction for IaC libraries that is both dynamic and straightforward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;To fail as many times as possible&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every time I fail when I think I have things right, I learn something new&lt;/li&gt;
&lt;li&gt;Failed IaC pipelines can sometimes be scary, and every failure I have on these project can teach me about potential failure modes for live projects running in production&lt;/li&gt;
&lt;li&gt;You can often times be "stuck" where you have a set of resources that you can't update or delete. Learning to get unstuck from these scenarios is important&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;To take an application-first approach to DevOps&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application developers are increasingly being tasked with operational duties&lt;/li&gt;
&lt;li&gt;While learning about IaC, I had a hard time finding in-depth materials covering application development, CI/CD pipelines and automation and Infrastructure as Code and how these three knowledge domains work together. There are important considerations to make when  between a Hello World docker image&lt;/li&gt;
&lt;li&gt;You could probably use another framework with these IaC libraries like Flask or Rails, but for now I'm building these projects with Django first-in-mind&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;To develop a project I can reference when helping myself and others&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;companies and projects that do IaC and CI/CD for the most part have things in private repos for obvious reasons, there isn't any good reason to share this type of code unless you are sharing it with an auditor&lt;/li&gt;
&lt;li&gt;Hopefully the sample application, IaC and CI/CD pipelines &lt;em&gt;aren't overly complex&lt;/em&gt;. There are more complex examples of open source companies out there, but their repos have steep learning curves and a lot going on&lt;/li&gt;
&lt;li&gt;People often ask about how to split up IaC deployments and application deployments. I want to be able to use this project to &lt;strong&gt;show&lt;/strong&gt; people how it can be done&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;To encourage others (specifically Developer Advocates / Developer Relations / Solutions Architects in the CDK, Terraform and Pulumi communities) to share complete and non-trivial examples of IaC software **in use&lt;/strong&gt; with an actual application.**&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There are many ways one could create "IaC Rosetta Stone" (&lt;code&gt;public cloud providers x CI/CD providers x IaC tools&lt;/code&gt; is a big number)&lt;/li&gt;
&lt;li&gt;This takes a lot of effort and time&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;I have nothing to sell you&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;So many articles about Cloud/DevOps are trying to sell you a tool. Outside of what I consider to be mainstream vendors like GitHub and AWS, there are no products that I'm promoting here&lt;/li&gt;
&lt;li&gt;I'm also not trying to sell anyone on using my IaC packages&lt;/li&gt;
&lt;li&gt;Hopefully my IaC packages can serve as helpful reference or starting point&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Walk before running&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I want to build up confidence with vanilla use cases before getting too fancy&lt;/li&gt;
&lt;li&gt;With a solid foundation in these tools, I want to learn about some of the more advanced patterns teams are adopting (Pulumi Automation API, Terragrunt for Terraform, self-mutating CDK Pipelines)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;12 Factor App, DevOps and Platform Engineering&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://12factor.net/" rel="noopener noreferrer"&gt;12 Factor App&lt;/a&gt; is great, and has guided how I approach both Django application development and IaC library development&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://platformengineering.org/" rel="noopener noreferrer"&gt;platformengineering.org&lt;/a&gt; community has some good guiding principles&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  CDK/Terraform/Pulumi terminology
&lt;/h2&gt;

&lt;h3&gt;
  
  
  constructs, modules and components
&lt;/h3&gt;

&lt;p&gt;A CDK construct, Terraform module and Pulumi component generally mean the same thing: an abstract grouping of one or more cloud resources.&lt;/p&gt;

&lt;p&gt;In this article I will refer to &lt;strong&gt;constructs/modules/components&lt;/strong&gt; as &lt;strong&gt;c/m/c&lt;/strong&gt; for short, and the term &lt;strong&gt;stack&lt;/strong&gt; can generally be used to refer to either a CloudFormation stack, a Pulumi Stack or a Terraform group of resources that are part of a module that has had &lt;code&gt;apply&lt;/code&gt; ran against it.&lt;/p&gt;

&lt;h3&gt;
  
  
  what is a stack?
&lt;/h3&gt;

&lt;p&gt;AWS has a resource type called CloudFormation Stacks, and Pulumi also has a concept of stacks. Terraform documentation doesn't refer to stacks, and instead in Terraform docs use the words "Terraform configuration" to refer to some group of resources that were built using a module.&lt;/p&gt;

&lt;p&gt;CDK Constructs and Pulumi Components are somewhat similar, however CDK Constructs map to CloudFormation and the Pulumi components I'm using from the &lt;code&gt;@pulumi/aws&lt;/code&gt; package generally map directly to Terraform resources from the AWS Provider (the Pulumi AWS Provider uses much of the same code that the Terraform AWS Provider uses).&lt;/p&gt;

&lt;h3&gt;
  
  
  verbs
&lt;/h3&gt;

&lt;p&gt;In CDK you &lt;code&gt;synth&lt;/code&gt; CDK code to generate CloudFormation templates. You can also run &lt;code&gt;diff&lt;/code&gt; to see what changes would be applied during a stack update.&lt;/p&gt;

&lt;p&gt;In Terraform you &lt;code&gt;init&lt;/code&gt; to download all providers and modules. This is sort of like running &lt;code&gt;npm install&lt;/code&gt; in CDK and Pulumi. You then run &lt;code&gt;terraform plan&lt;/code&gt; to see the changes that would result. &lt;code&gt;terraform apply&lt;/code&gt; does CRUD operations on your cloud resources.&lt;/p&gt;

&lt;p&gt;In Pulumi you run &lt;code&gt;pulumi preview&lt;/code&gt; to see what changes would be made to a stack. You can use the &lt;code&gt;--diff&lt;/code&gt; flag to see the specifics of what would change.&lt;/p&gt;

&lt;p&gt;To summarize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In CDK you synth CloudFormation and use these templates to deploy stacks made up of constructs. An "app" can contain multiple stacks, and you can deploy one or more stacks in an app at a time&lt;/li&gt;
&lt;li&gt;In Terraform you plan a configuration made up of modules, and then run &lt;code&gt;terraform apply&lt;/code&gt; to build the configuration/stack (&lt;a href="https://discuss.hashicorp.com/t/what-is-a-terraform-stack/31985" rel="noopener noreferrer"&gt;discuss.hashicorp.com/t/what-is-a-terraform-stack/31985&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Pulumi: You preview a Pulumi stack made up of components, and then run &lt;code&gt;pulumi up&lt;/code&gt; to build the resources&lt;/li&gt;
&lt;li&gt;To tear down a stack in all three tools, you run &lt;code&gt;destroy&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Infrastructure as Code library repos
&lt;/h2&gt;

&lt;p&gt;Let's look at the three repos that I wrote for deploying the same type of 3-tier web application to AWS using ECS Fargate.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CDK: &lt;a href="https://github.com/briancaffey/cdk-django" rel="noopener noreferrer"&gt;&lt;code&gt;cdk-django&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Terraform: &lt;a href="https://github.com/briancaffey/terraform-aws-django" rel="noopener noreferrer"&gt;&lt;code&gt;terraform-aws-django&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Pulumi: &lt;a href="https://github.com/briancaffey/pulumi-aws-django" rel="noopener noreferrer"&gt;&lt;code&gt;pulumi-aws-django&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Language
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;cdk-django&lt;/code&gt; and &lt;code&gt;pulumi-aws-django&lt;/code&gt; are both written in TypeScript. &lt;code&gt;terraform-aws-django&lt;/code&gt; is written in HCL, a domain specific language created by HashiCorp. The &lt;code&gt;cdk-django&lt;/code&gt; is published to both npm and PyPI, so you can use it in JavaScript, TypeScript and Python projects, other languages are supported as well, but you need to write your library in TypeScript so it can be transpiled to other languages using &lt;a href="https://github.com/aws/jsii" rel="noopener noreferrer"&gt;jsii&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My Pulumi library is written in TypeScript and is published to NPM. For now it can only be used in JavaScript and TypeScript projects. There is a way in Pulumi to write in any language and then publish to any other major language, but I haven't  done this yet. See &lt;a href="https://github.com/pulumi/pulumi-component-provider-ts-boilerplate" rel="noopener noreferrer"&gt;this GitHub repo&lt;/a&gt; for more information on this.&lt;/p&gt;

&lt;p&gt;The HCL is pretty simple when you get used to it. I find that I don't like adding lots of logic in Terraform code because it takes away from the readability of a module. There is a tool called &lt;a href="https://developer.hashicorp.com/terraform/cdktf" rel="noopener noreferrer"&gt;CDKTF&lt;/a&gt; which allows you to write HCL Terraform in TypeScript, but I haven't used it yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  Release management, versioning and publishing
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pulumi-aws-django&lt;/code&gt; and &lt;code&gt;terraform-aws-django&lt;/code&gt; both use &lt;code&gt;release-please&lt;/code&gt; for automatically generating a changelog file and bumping versions. &lt;code&gt;release-please&lt;/code&gt; is an open source tool from Google that they use to version their Terraform GCP modules. Whenever I push new commits to &lt;code&gt;main&lt;/code&gt;, a new PR is created that adds changes to the CHANGELOG.md file, bumps the version of the library in &lt;code&gt;package.json&lt;/code&gt; and adds a new git tag (e.g. &lt;code&gt;v1.2.3&lt;/code&gt;) based on commit messages.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;cdk-django&lt;/code&gt; uses &lt;a href="https://github.com/projen/projen" rel="noopener noreferrer"&gt;&lt;code&gt;projen&lt;/code&gt;&lt;/a&gt; for maintaining the changelog and bumping versions and publishing to npm. It is popular among developers in the CDK community and is a really awesome tool since it basically uses one file (&lt;code&gt;.projenrc.ts&lt;/code&gt;) to configure your entire repo, including files like &lt;code&gt;tsconfig.json&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;, and even GitHub Action workflows. It has a lot of configuration options, but I'm using it in a pretty simple way. It generates a new release and items to the changelog when I manually trigger a GitHub Action.&lt;/p&gt;

&lt;p&gt;These tools are both based on &lt;a href="https://www.conventionalcommits.org/en/v1.0.0/" rel="noopener noreferrer"&gt;conventional commits&lt;/a&gt; to automatically update the Changelog file.&lt;/p&gt;

&lt;p&gt;I'm still manually publishing my &lt;code&gt;pulumi-aws-django&lt;/code&gt; package from the CLI. I need to add a GitHub Action to do this for me. This and other backlog items are listed at the end of the article!&lt;/p&gt;

&lt;h3&gt;
  
  
  Makefile, examples and local development
&lt;/h3&gt;

&lt;p&gt;Each repo has a Makefile that includes commands that I frequently use when developing new features or fixing bugs. Each repo has commands for the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;synthesizing CDK to CloudFormation / running &lt;code&gt;terraform plan&lt;/code&gt; / previewing pulumi up for both the base and app stacks&lt;/li&gt;
&lt;li&gt;creating/updating an ad hoc base stack called &lt;code&gt;dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;destroying resources in the ad-hoc base stack called &lt;code&gt;dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;creating an ad hoc app stack called &lt;code&gt;alpha&lt;/code&gt; that uses resources from &lt;code&gt;dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;destroying an ad hoc app stack called &lt;code&gt;alpha&lt;/code&gt; that uses resources from &lt;code&gt;dev&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;creating/updating a prod base stack called &lt;code&gt;stage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;destroying resources in the prod base stack called &lt;code&gt;stage&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;creating a prod app stack using called &lt;code&gt;stage&lt;/code&gt; that uses resources from the &lt;code&gt;stage&lt;/code&gt; base stack&lt;/li&gt;
&lt;li&gt;destroying resources in the prod app stack called &lt;code&gt;stage&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's an example of what these commands look like in &lt;code&gt;pulumi-aws-django&lt;/code&gt; for prod infrastructure base and app stacks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;prod-base-preview&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/base &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; preview

&lt;span class="nl"&gt;prod-base-up&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/base &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; up &lt;span class="nt"&gt;--yes&lt;/span&gt;

&lt;span class="nl"&gt;prod-base-destroy&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/base &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; destroy &lt;span class="nt"&gt;--yes&lt;/span&gt;

&lt;span class="nl"&gt;prod-app-preview&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/app &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; preview

&lt;span class="nl"&gt;prod-app-preview-diff&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;  &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/app &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; preview &lt;span class="nt"&gt;--diff&lt;/span&gt;

&lt;span class="nl"&gt;prod-app-up&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/app &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; up &lt;span class="nt"&gt;--yes&lt;/span&gt;

&lt;span class="nl"&gt;prod-app-destroy&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;build&lt;/span&gt;
    pulumi &lt;span class="nt"&gt;-C&lt;/span&gt; examples/prod/app &lt;span class="nt"&gt;--stack&lt;/span&gt; stage &lt;span class="nt"&gt;--non-interactive&lt;/span&gt; destroy &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I currently don't have tests for all of these libraries, but for now the most effective way of testing that things are working correctly is to use the &lt;code&gt;c/m/c&lt;/code&gt;s to create environments and smoke check the environments to make sure everything works correctly.&lt;/p&gt;

&lt;p&gt;Adding unit tests is another item for the backlog.&lt;/p&gt;

&lt;h3&gt;
  
  
  ad-hoc vs prod
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://briancaffey.github.io/2022/03/27/ad-hoc-developer-environments-for-django-with-aws-ecs-terraform-and-github-actions" rel="noopener noreferrer"&gt;the last article I wrote was about ad hoc environments&lt;/a&gt;. Also known as "on-demand" environments or "preview" environments.&lt;/li&gt;
&lt;li&gt;the motivation for using ad-hoc environments is speed and cost (you can stand up an environment in less time and you share the costs of the base environment, including VPC, ALB, RDS)&lt;/li&gt;
&lt;li&gt;you can completely ignore "ad-hoc" environments and use the "prod" infrastructure for any number of environments (such as dev, QA, RC, stage and prod)&lt;/li&gt;
&lt;li&gt;prod can be used for a production environment and any number of pre-production environments&lt;/li&gt;
&lt;li&gt;multiple environments built with "prod" infrastructure can be configured with a "knobs and dials" (e.g., how big are app and DB instances, how many tasks to run in a service, etc.)&lt;/li&gt;
&lt;li&gt;the "prod" infrastructure should be the same for the "production" environment and the "staging" environment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Directory structure
&lt;/h3&gt;

&lt;p&gt;The directory structures for each repo are all similar with some minor differences.&lt;/p&gt;

&lt;p&gt;There are two types of environments: &lt;code&gt;ad-hoc&lt;/code&gt; and &lt;code&gt;prod&lt;/code&gt;. Within ad-hoc and production, there are two directories &lt;code&gt;base&lt;/code&gt; and &lt;code&gt;app&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each repo has a directory called &lt;code&gt;internal&lt;/code&gt; which contain building blocks used by the &lt;code&gt;c/m/c&lt;/code&gt;s that are exposed. The contents of the &lt;code&gt;internal&lt;/code&gt; directories are not intended to be used by anyone who is using the libraries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDK construct library repo structure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/cdk-django$ tree -L 4 -d src/
src/
├── constructs
│   ├── ad-hoc
│   │   ├── app
│   │   └── base
│   ├── internal
│   │   ├── alb
│   │   ├── bastion
│   │   ├── customResources
│   │   │   └── highestPriorityRule
│   │   ├── ecs
│   │   │   ├── iam
│   │   │   ├── management-command
│   │   │   ├── redis
│   │   │   ├── scheduler
│   │   │   ├── web
│   │   │   └── worker
│   │   ├── rds
│   │   ├── sg
│   │   └── vpc
│   └── prod
│       ├── app
│       └── base
└── examples
    └── ad-hoc
        ├── app
        │   └── config
        └── base
            └── config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Terraform module library repo structure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/terraform-aws-django$ tree -L 4 -d modules
modules
├── ad-hoc
│   ├── app
│   └── base
├── internal
│   ├── alb
│   ├── autoscaling
│   ├── bastion
│   ├── ecs
│   │   ├── ad-hoc
│   │   │   ├── celery_beat
│   │   │   ├── celery_worker
│   │   │   ├── cluster
│   │   │   ├── management_command
│   │   │   ├── redis
│   │   │   └── web
│   │   └── prod
│   │       ├── celery_beat
│   │       ├── celery_worker
│   │       ├── cluster
│   │       ├── management_command
│   │       └── web
│   ├── elasticache
│   ├── iam
│   ├── rds
│   ├── route53
│   ├── s3
│   ├── sd
│   └── sg
└── prod
    ├── app
    └── base
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pulumi component library repo structure&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/pulumi-aws-django$ tree -L 3 src/
src/
├── components
│   ├── ad-hoc
│   │   ├── README.md
│   │   ├── app
│   │   └── base
│   └── internal
│       ├── README.md
│       ├── alb
│       ├── bastion
│       ├── cw
│       ├── ecs
│       ├── iam
│       ├── rds
│       └── sg
└── util
    ├── index.ts
    └── taggable.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Pulumi examples directory&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/pulumi-aws-django$ tree -L 3 examples/
examples/
└── ad-hoc
    ├── app
    │   ├── Pulumi.alpha.yaml
    │   ├── Pulumi.yaml
    │   ├── index.ts
    │   ├── node_modules
    │   ├── package-lock.json
    │   ├── package.json
    │   └── tsconfig.json
    └── base
        ├── Pulumi.yaml
        ├── bin
        ├── index.ts
        ├── package-lock.json
        ├── package.json
        └── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  CLOC
&lt;/h3&gt;

&lt;p&gt;Let's use CLOC (count lines of code) to compare the lines of code used in the &lt;code&gt;c/m/c&lt;/code&gt; of CDK/CloudFormation/Terraform/Pulumi.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;cdk-django&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/cdk-django$ cloc src/constructs/
      14 text files.
      14 unique files.
       0 files ignored.

github.com/AlDanial/cloc v 1.94  T=0.04 s (356.1 files/s, 30040.9 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
TypeScript                      13            155             59            908
Python                           1             18              8             33
-------------------------------------------------------------------------------
SUM:                            14            173             67            941
-------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;terraform-aws-django&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/terraform-aws-django$ cloc modules/
      68 text files.
      58 unique files.
      11 files ignored.

github.com/AlDanial/cloc v 1.94  T=0.15 s (385.9 files/s, 20585.1 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
HCL                             55            472            205           2390
Markdown                         3              7              0             20
-------------------------------------------------------------------------------
SUM:                            58            479            205           2410
-------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;pulumi-aws-django&lt;/code&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;~/git/github/pulumi-aws-django$ cloc src/components/
      15 text files.
      15 unique files.
       0 files ignored.

github.com/AlDanial/cloc v 1.94  T=0.11 s (134.5 files/s, 12924.2 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
TypeScript                      13            110            176           1119
Markdown                         2              6              0             30
-------------------------------------------------------------------------------
SUM:                            15            116            176           1149
-------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Communities
&lt;/h2&gt;

&lt;p&gt;The CDK, Terraform and Pulumi communities are all great and a lot of people helped when I got stuck on issues writing these libraries. Thank you!&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://cdk.dev/" rel="noopener noreferrer"&gt;cdk.dev&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discuss.hashicorp.com/c/terraform-core/27" rel="noopener noreferrer"&gt;Terraform Section of HashiCorp Discuss Forum&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://slack.pulumi.com/" rel="noopener noreferrer"&gt;Pulumi Slack&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  μblog
&lt;/h2&gt;

&lt;p&gt;μblog is a micro blogging application that I have written using Django and Vue.js. Here's a screenshot of the homepage:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsndugpmfr81w3dd7qjxt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsndugpmfr81w3dd7qjxt.png" alt="ublog" width="800" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is a pretty simple app. Users can write posts with text and an optional images. Logged in users can write posts and like posts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mono-repo structure
&lt;/h3&gt;

&lt;p&gt;It lives in a GitHub mono repo called &lt;code&gt;django-step-by-step&lt;/code&gt;. This mono repo contains a few different things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend Django application&lt;/li&gt;
&lt;li&gt;frontend Vue.js application&lt;/li&gt;
&lt;li&gt;IaC code that uses c/m/c from &lt;code&gt;cdk-django&lt;/code&gt;, &lt;code&gt;terraform-aws-django&lt;/code&gt; and &lt;code&gt;pulumi-aws-django&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Actions workflows for both Infrastructure deployments and application deployments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;μblog is the reference application that I deploy to infrastructure created with CDK, Terraform and Pulumi. μblog is meant to represent a generic 12 Factor application that uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gunicorn for a backend API&lt;/li&gt;
&lt;li&gt;Vue.js for a client that consumes the backend API&lt;/li&gt;
&lt;li&gt;celery for async task processing&lt;/li&gt;
&lt;li&gt;celery beat for scheduling tasks&lt;/li&gt;
&lt;li&gt;Postgres for relational data&lt;/li&gt;
&lt;li&gt;Redis for caching and message brokering&lt;/li&gt;
&lt;li&gt;S3 for object storage&lt;/li&gt;
&lt;li&gt;Django admin for a simple admin interface&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a lot more I could add on μblog. For now I'll just mention that it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;has a great local development environment (supports both docker-compose and virtual environments)&lt;/li&gt;
&lt;li&gt;demonstrates how to use Django in different ways. It implements the same application using Function Based View and Class Based Views, and implements both a REST API (both with FBV and CBV) and GraphQL.&lt;/li&gt;
&lt;li&gt;GitHub Actions for running unit tests&lt;/li&gt;
&lt;li&gt;k6 for load testing&lt;/li&gt;
&lt;li&gt;contains a documentation site deployed to GitHub pages (made with VuePress) can be found here: &lt;a href="https://briancaffey.github.io/django-step-by-step/" rel="noopener noreferrer"&gt;https://briancaffey.github.io/django-step-by-step/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Infrastructure Deep Dive
&lt;/h1&gt;

&lt;p&gt;Let's go through each of the &lt;code&gt;c/m/c&lt;/code&gt;s used in the three libraries. I'll cover some of the organizational decisions, dependencies and differences between how things are done between CDK, Terraform and Pulumi.&lt;/p&gt;

&lt;p&gt;I'll first talk about the two stacks used in ad hoc environments: &lt;code&gt;base&lt;/code&gt; and &lt;code&gt;app&lt;/code&gt;. Then I'll talk about the prod environments which are also composed of &lt;code&gt;base&lt;/code&gt; and &lt;code&gt;app&lt;/code&gt; stacks.&lt;/p&gt;

&lt;p&gt;Keep in mind that there aren't that many differences between the ad hoc environment base and app stacks and the prod environment app and base stacks. A future optimization could be to use a single base and app stack, but I think there is a trade-off between readability and DRYness of infrastructure code, &lt;strong&gt;especially with Terraform&lt;/strong&gt;. In general I try to use very little conditionals and logic with Terraform code. It is much easier to have dynamic configuration in CDK and Pulumi, and probably also for other tools like CDKTF (that I have not yet tried).&lt;/p&gt;

&lt;h2&gt;
  
  
  Splitting up the stacks
&lt;/h2&gt;

&lt;p&gt;While it is possible to put all resources in a single stack with both Terraform, CDK and Pulumi, it is not recommended to do so.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Terraform enables this with outputs and &lt;code&gt;terraform_remote_state&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pulumi encourages the use of &lt;a href="https://www.pulumi.com/docs/guides/organizing-projects-stacks/" rel="noopener noreferrer"&gt;micro stacks&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CDK has an article on how to &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/stack_how_to_create_multiple_stacks.html" rel="noopener noreferrer"&gt;create an app with multiple stacks&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My design decision was to keep things limited to 2 stacks. Later on it would be interesting to try splitting out another stack.&lt;/p&gt;

&lt;p&gt;Also, on-demand environments really lends itself to stacks that are split up.&lt;/p&gt;

&lt;p&gt;In the section &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/resources.html" rel="noopener noreferrer"&gt;"Passing unique identifiers"&lt;/a&gt;, the CDK recommends that we keep the two stacks in the same app. In Terraform and Pulumi, each stack environment is in its own app.&lt;/p&gt;

&lt;p&gt;There is a balance to be found between single stacks vs micro stacks. Both the base and app &lt;code&gt;c/m/c&lt;/code&gt;s could be split out further. For example, the &lt;code&gt;base&lt;/code&gt; &lt;code&gt;c/m/c&lt;/code&gt;s could be split into &lt;code&gt;networking&lt;/code&gt; and &lt;code&gt;rds&lt;/code&gt;. The &lt;code&gt;app&lt;/code&gt; stack could be split into different ECS services so that their infrastructure can be deployed independently, like &lt;code&gt;cluster&lt;/code&gt;, &lt;code&gt;backend&lt;/code&gt; and &lt;code&gt;frontend&lt;/code&gt;. The more resources that a stack has, the longer it takes to deploy and the more risky it gets, but adding lots of stacks can add to mental overhead, and pipeline complexity. Each tool has ways of dealing with these complexities (CDK Pipelines, Terragrunt, Pulumi Automation API), but I won't be getting into any of these options in this article. I would like to try these out and share in a future article.&lt;/p&gt;

&lt;p&gt;My rules of thumbs are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;single stacks are bad because you don't want to put all your eggs in one basket, however your IaC tool should give you confidence about what is going to change when you try to make a change&lt;/li&gt;
&lt;li&gt;Lots of small stacks can cause overhead and make things more complex than they need to be&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Ad hoc base overview
&lt;/h2&gt;

&lt;p&gt;Here's an overview of the resources used in an ad hoc base environment.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;(Inputs)&lt;/li&gt;
&lt;li&gt;(Optional environment configs)&lt;/li&gt;
&lt;li&gt;VPC and Service Discovery&lt;/li&gt;
&lt;li&gt;S3&lt;/li&gt;
&lt;li&gt;Security Groups&lt;/li&gt;
&lt;li&gt;Load Balancer&lt;/li&gt;
&lt;li&gt;RDS&lt;/li&gt;
&lt;li&gt;Bastion Host&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Visualization
&lt;/h3&gt;

&lt;p&gt;Here's a dependency graph showing all of the resources in ad hoc base stack. This can be found on the &lt;code&gt;Resources&lt;/code&gt; tab of the ad hoc base stack in the Pulumi console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu85q1p5h5n7fw9bgs370.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu85q1p5h5n7fw9bgs370.png" alt="Graph view of ad hoc base infrastructure" width="800" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Inputs
&lt;/h3&gt;

&lt;p&gt;There are only two required inputs for the ad hoc base stack&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ACM certificate ARN&lt;/li&gt;
&lt;li&gt;Domain Name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I store these values in environment variables for the pipelines in CDK, Terraform and Pulumi. When running pipelines from my local environment, they are exported in my shell before running deploy/apply/up or synth/plan/preview.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPC
&lt;/h3&gt;

&lt;p&gt;The VPC is the first resource that is created as part of the &lt;code&gt;base&lt;/code&gt; stack. There official, high-level constructs in each IaC tool for building VPCs and all related networking resources.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;awsx&lt;/code&gt; has a VPC module&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;terraform-aws-vpc&lt;/code&gt; module&lt;/li&gt;
&lt;li&gt;L2 VPC Construct in CDK&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The setting in the Terraform VPC module &lt;code&gt;one_nat_gateway_per_az = false&lt;/code&gt; doesn't seem to exist on the &lt;code&gt;awsx.ec2.Vpc&lt;/code&gt; module. This will add to cost savings since it will use 1 NAT Gateway instead of 2 or 3.&lt;/p&gt;

&lt;h3&gt;
  
  
  Security Groups
&lt;/h3&gt;

&lt;p&gt;Pulumi and Terraform can be used in a similar way to define security groups. CDK has a much more concise option for defining ingress and egress rules for security groups.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;albSecurityGroup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;SecurityGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AlbSecurityGroup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;albSecurityGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addIngressRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;anyIpv4&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;443&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTPS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;albSecurityGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addIngressRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Peer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;anyIpv4&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nx"&gt;Port&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tcp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HTTP&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Load Balancer Resources
&lt;/h3&gt;

&lt;p&gt;There's not much to comment on here. In each library I have resource group that defines the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application Load Balancer&lt;/li&gt;
&lt;li&gt;A default target group&lt;/li&gt;
&lt;li&gt;An HTTP listener that redirects to HTTPS&lt;/li&gt;
&lt;li&gt;An HTTPS listener with a default "fixed-response" action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Properties from these resources are used in the "app" stack to build listener rules for ECS services that are configured with load balancers, such as the backend and frontend web services.&lt;/p&gt;

&lt;p&gt;Ad hoc app environments all share a common load balancer from the base stack that they use.&lt;/p&gt;

&lt;h3&gt;
  
  
  RDS Resources
&lt;/h3&gt;

&lt;p&gt;All three libraries have the RDS security group and Subnet Group in the same &lt;code&gt;c/m/c&lt;/code&gt; as the RDS instance. The SG and DB Subnet group could alternatively be grouped closer to the other network resources.&lt;/p&gt;

&lt;p&gt;Currently the RDS resources are part of the "base" stack for each library. A future optimization may be to break the RDS instance out of the "base" stack and put it in its own stack. The "RDS" stack would be dependent on the "base" stack, and then "app" stack would then be dependent on both the "base" stack and the "RDS" stack. More stacks isn't necessarily a bad thing, but for my initial implementation of these libraries I have decided to keep the "micro stacks" approach limited to only 2 stacks for an environment.&lt;/p&gt;

&lt;p&gt;The way that database secrets are handled is another difference between CDK and Terraform and Pulumi. I am currently "hardcoding" the RDS password for Terraform and Pulumi, and in CDK I am using a Secrets Manager Secret for the database credential.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;secret&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dbSecret&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dbSecretName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;secret for rds&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;generateSecretString&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;secretStringTemplate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;postgres&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
        &lt;span class="na"&gt;generateStringKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;password&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;excludePunctuation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;includeSpace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;code&gt;DatabaseInstance&lt;/code&gt; props we can then use this secret like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    credentials: Credentials.fromSecret(secret),
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the application deployed with CDK, I use a Django settings module package that uses a package called &lt;code&gt;aws_secretsmanager_caching&lt;/code&gt; to get and cache the secrets manager secret for the database, whereas in the apps deployed with Terraform and Pulumi I read in the password from an environment variable.&lt;/p&gt;

&lt;p&gt;The Terraform and Pulumi database instance arguments simply accept a &lt;code&gt;password&lt;/code&gt; field. This will be another item for the backlog for Terraform and Pulumi. The &lt;a href="https://www.pulumi.com/registry/packages/random/api-docs/randompassword/" rel="noopener noreferrer"&gt;&lt;code&gt;randompassword&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://www.pulumi.com/registry/packages/aws/api-docs/secretsmanager/secretversion/" rel="noopener noreferrer"&gt;&lt;code&gt;secretversion&lt;/code&gt;&lt;/a&gt; components can be used to do this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bastion Host
&lt;/h3&gt;

&lt;p&gt;There are two main use cases for the bastion host in ad-hoc environments.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;When creating a new ad hoc app environment, the bastion host is used to create a new database called &lt;code&gt;{ad-hoc-env-name}-db&lt;/code&gt; that the new ad hoc environment will use. (There might be another way of doing this, but using a bastion host is working well for now).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If you using a database management tool on you local machine like DBeaver, the bastion host can help you connect to the RDS instance in a private subnet. The bastion host instance is configured to run a service that forwards traffic on port 5432 to the RDS instance. If you port forward from your local machine to the bastion host on port 5432, you can connect RDS by simple connecting to &lt;code&gt;localhost:5432&lt;/code&gt; on your local machine.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to manage SSH keys since you connect to the instance in a private subnet using SSM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ssm start-session &lt;span class="nt"&gt;--target&lt;/span&gt; &lt;span class="nv"&gt;$INSTANCE_ID&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Outputs
&lt;/h3&gt;

&lt;p&gt;Here are the outputs for the ad hoc base stack used in Terraform and Pulumi:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vpc_id&lt;/li&gt;
&lt;li&gt;assets_bucket_name&lt;/li&gt;
&lt;li&gt;private_subnet_ids&lt;/li&gt;
&lt;li&gt;app_sg_id&lt;/li&gt;
&lt;li&gt;alb_sg_id&lt;/li&gt;
&lt;li&gt;listener_arn&lt;/li&gt;
&lt;li&gt;alb_dns_name&lt;/li&gt;
&lt;li&gt;task_role_arn&lt;/li&gt;
&lt;li&gt;execution_role_arn&lt;/li&gt;
&lt;li&gt;rds_address&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In CDK, the stack references in the app stack don't reference the unique identifiers from the base stack (such as the VPC id or bastion host instance id), but instead they reference the properties of the stack which have types like &lt;code&gt;Vpc&lt;/code&gt; and &lt;code&gt;RdsInstance&lt;/code&gt;. More on this later in the following section &lt;strong&gt;Passing data between stacks&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ad hoc app overview
&lt;/h2&gt;

&lt;p&gt;The ad hoc app is an group of resources that powers an on-demand environment that is meant to be short lived for testing, QA, validation, demos, etc.&lt;/p&gt;

&lt;p&gt;This visualization shows all of the resources in the ad hoc app stack. It also comes from the Pulumi console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc0po3apmpln1twxao26s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc0po3apmpln1twxao26s.png" alt="Graph view of ad hoc app infrastructure" width="800" height="678"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ECS Cluster
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;This is a small component that defines both ECS Cluster and the default capacity providers&lt;/li&gt;
&lt;li&gt;It defaults to not using &lt;code&gt;FARGATE_SPOT&lt;/code&gt;; ad hoc environments do use &lt;code&gt;FARGATE_SPOT&lt;/code&gt; for cost savings&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;NOTE: defaultCapacityProviderStrategy on cluster not currently supported. (&lt;a href="https://docs.aws.amazon.com/cdk/api/v1/docs/@aws-cdk_aws-ecs.CapacityProviderStrategy.html" rel="noopener noreferrer"&gt;link&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Shared environment variables
&lt;/h3&gt;

&lt;p&gt;The backend containers should all have the same environment variables, so I define them once in the app stack and pass these into the service resource &lt;code&gt;c/m/c&lt;/code&gt;s.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I struggled to get this right in pulumi. A lot of Pulumi examples used &lt;code&gt;JSON.stringify&lt;/code&gt; for containerDefinitions in task definitions. I was able to get help from the Pulumi Slack channel; someone recommended that I use &lt;code&gt;pulumi.jsonStringify&lt;/code&gt; which was added in a relatively recent version of &lt;code&gt;pulumi/pulumi&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;CDK allows you to declare environment variables for a containerDefinition like &lt;code&gt;{ FOO: "bar" }&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Pulumi and Terraform require that values are passed like &lt;code&gt;{ name: "FOO", value: "bar"}&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You could transform &lt;code&gt;{ FOO: "bar" }&lt;/code&gt; into the name/value format, but I didn't bother to do this&lt;/li&gt;
&lt;li&gt;extra env vars in Terraform to allow for dynamically passing extra environment variables, and I used the &lt;code&gt;concat&lt;/code&gt; function to add these to the list of default environment variables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's what the code looks like for joining extra environment variables to the default environment variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="c1"&gt;// CDK&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;extraEnvVars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;environmentVariables&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;extraEnvVars&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;environmentVariables&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    # terraform
    env_vars = concat(local.env_vars, var.extra_env_vars)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    // Pulumi
    if (extraEnvVars) {
      envVars = envVars.apply(x =&amp;gt; x.concat(extraEnvVars!))
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Route53 Record
&lt;/h3&gt;

&lt;p&gt;This is pretty straightforward in each library. Each ad hoc environment gets a Route 53 record, and listener rules for the web services (Django and Vue.js SPA) match on a combination of the host header and path patterns.&lt;/p&gt;

&lt;p&gt;This part is pretty opinionated in that it assumes you want to host the frontend and backend services on the same URL. For example, requests matching &lt;code&gt;example.com/api/*&lt;/code&gt; are routed to the backend API and all other requests matching &lt;code&gt;example.com/*&lt;/code&gt; are routed to the frontend service.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis
&lt;/h3&gt;

&lt;p&gt;I go into more depth about why I run a Redis instance in an ECS service in my other article. This is only for the ad hoc environments. Production environments are configured with ElastiCache running Redis.&lt;/p&gt;

&lt;p&gt;I decided to not make this service use any persistent storage. It may be a good idea to not use FARGATE_SPOT for this service, since restarts to the redis service could cause issues in ad hoc environments. For example, you may get a lot of celery errors in ad hoc environments if redis is not reachable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Web Service
&lt;/h3&gt;

&lt;p&gt;The web service is what defines the main Django application as well as the frontend website (JavaScript SPA or SSR site). I designed the Web Service resources group to be able to support both traditional Django apps (powered by templates), or for Django apps that service only a limited number of endpoints. This &lt;code&gt;c/m/c&lt;/code&gt; has an input parameter called &lt;code&gt;pathPatterns&lt;/code&gt; which determines which paths it serves. For example, the API container may serve traffic for &lt;code&gt;/api/*&lt;/code&gt; and &lt;code&gt;/admin/*&lt;/code&gt; only, or it may want to serve all traffic (&lt;code&gt;/*&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The way I use these components in ad hoc and prod environments is heavily opinionated in that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it assumes that the frontend SPA/SSR site should have a lower priority rule than the backend service and should route request paths matching &lt;code&gt;/*&lt;/code&gt;, while the backend service routes requests for a specific list of path patterns (&lt;code&gt;/api/*&lt;/code&gt;, &lt;code&gt;/admin/*&lt;/code&gt;, &lt;code&gt;/graphql/*&lt;/code&gt;, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You may want Django to handle most of your routes and 404 pages, in which case you would want the SPA to only handle requests matching certain paths. This would require some more consideration and careful refactoring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Celery
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The reason for having a celery service is to be able to have potentially multiple workers that scale independently&lt;/li&gt;
&lt;li&gt;I use the same Pulumi component for both works and schedulers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The terminology for this resource group could be better. Celery is one of many options for running async task workers, so it should probably be called something like &lt;code&gt;AsyncWorker&lt;/code&gt; across the board rather than using the term &lt;code&gt;celery&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Management Command
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Defines a task that can be used to run commands like &lt;code&gt;collectstatic&lt;/code&gt; and &lt;code&gt;migrate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;These tasks are ran both after the initial &lt;code&gt;app&lt;/code&gt; stack deployment and before rolling application upgrades&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my Django app I have a single management command that calls &lt;code&gt;migrate&lt;/code&gt; and &lt;code&gt;collectstatic&lt;/code&gt; and runs them in the same process one after another. This management command could also be used for clearing caches during updates, loading fixtures, etc.&lt;/p&gt;

&lt;p&gt;One other thing to note about this &lt;code&gt;c/m/c&lt;/code&gt; is that it outputs a complete script that can be used in GitHub Actions (or on your CLI when testing locally) that does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;saves the &lt;code&gt;START&lt;/code&gt; timestamp&lt;/li&gt;
&lt;li&gt;runs the task with the required settings&lt;/li&gt;
&lt;li&gt;waits for the task to complete&lt;/li&gt;
&lt;li&gt;saves the &lt;code&gt;END&lt;/code&gt; timestamp&lt;/li&gt;
&lt;li&gt;collects the logs for the task between &lt;code&gt;START&lt;/code&gt; and &lt;code&gt;END&lt;/code&gt; and prints them to &lt;code&gt;stdout&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's an example of what the script looks like in Pulumi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;executionScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt;&lt;span class="s2"&gt;`#!/bin/bash
START_TIME=$(date +%s000)
TASK_ID=$(aws ecs run-task --cluster &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecsClusterId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; --task-definition &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;taskDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; --launch-type FARGATE --network-configuration "awsvpcConfiguration={subnets=[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;privateSubnetIds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))}&lt;/span&gt;&lt;span class="s2"&gt;],securityGroups=[&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appSgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;],assignPublicIp=ENABLED}" | jq -r '.tasks[0].taskArn')
aws ecs wait tasks-stopped --tasks $TASK_ID --cluster &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ecsClusterId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
END_TIME=$(date +%s000)
aws logs get-log-events --log-group-name &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;cwLoggingResources&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cwLogGroupName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; --log-stream-name &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{TASK_ID##*/} --start-time $START_TIME --end-time $END_TIME | jq -r '.events[].message'
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;executionScript&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;executionScript&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In GitHub Actions we get this command as a stack output, save it to a file, make it executable and then run it. This is what it looks like with CDK as a CloudFormation stack output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;backend&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;update&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;command"&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run_backend_update&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;# get the script from the stack output with an output key that contains the string `backendUpdate`&lt;/span&gt;
          &lt;span class="s"&gt;BACKEND_UPDATE_SCRIPT=$(aws cloudformation describe-stacks \&lt;/span&gt;
            &lt;span class="s"&gt;--stack-name $AD_HOC_APP_NAME \&lt;/span&gt;
            &lt;span class="s"&gt;| jq -r '.Stacks[0].Outputs[]|select(.OutputKey | contains("backendUpdate")) | .OutputValue' \&lt;/span&gt;
          &lt;span class="s"&gt;)&lt;/span&gt;

          &lt;span class="s"&gt;echo "$BACKEND_UPDATE_SCRIPT" &amp;gt; backend_update_command.sh&lt;/span&gt;
          &lt;span class="s"&gt;cat backend_update_command.sh&lt;/span&gt;
          &lt;span class="s"&gt;sudo chmod +x backend_update_command.sh&lt;/span&gt;
          &lt;span class="s"&gt;./backend_update_command.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Passing data between stacks
&lt;/h3&gt;

&lt;p&gt;Pulumi uses stack references, Terraform uses remote state and CDK uses Stack Outputs or Stack References.&lt;/p&gt;

&lt;p&gt;Here's what this looks like in Terraform&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data "terraform_remote_state" "this" {
  backend = "local"

  config = {
    path = "../base/terraform.tfstate"
  }
}

module "main" {
  source = "../../../modules/ad-hoc/app"

  vpc_id                         = data.terraform_remote_state.this.outputs.vpc_id
  assets_bucket_name             = data.terraform_remote_state.this.outputs.assets_bucket_name
  private_subnet_ids             = data.terraform_remote_state.this.outputs.private_subnet_ids
  app_sg_id                      = data.terraform_remote_state.this.outputs.app_sg_id
  alb_sg_id                      = data.terraform_remote_state.this.outputs.alb_sg_id
  listener_arn                   = data.terraform_remote_state.this.outputs.listener_arn
  alb_dns_name                   = data.terraform_remote_state.this.outputs.alb_dns_name
  service_discovery_namespace_id = data.terraform_remote_state.this.outputs.service_discovery_namespace_id
  rds_address                    = data.terraform_remote_state.this.outputs.rds_address
  domain_name                    = data.terraform_remote_state.this.outputs.domain_name
  base_stack_name                = data.terraform_remote_state.this.outputs.base_stack_name
  region                         = var.region
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CDK:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseStack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleAdHocBaseStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;baseStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appStack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleAdHocAppStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocAppEnvName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adHocAppEnvConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AdHocBase&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AdHocBase&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;certificateArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;domainName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;addHocApp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AdHocApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AdHocApp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;baseStackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vpc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;alb&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;appSecurityGroup&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;appSecurityGroup&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;serviceDiscoveryNamespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;serviceDiscoveryNamespace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;rdsInstance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;databaseInstance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;assetsBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;assetsBucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;domainName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;domainName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listener&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and in Pulumi:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;StackReference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;org&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/ad-hoc-base/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;vpcId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;vpcId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;assetsBucketName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assetsBucketName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;privateSubnets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;privateSubnetIds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appSgId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;appSgId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;albSgId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;albSgId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;listenerArn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;listenerArn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;albDnsName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;albDnsName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;serviceDiscoveryNamespaceId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serviceDiscoveryNamespaceId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rdsAddress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;rdsAddress&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;domainName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;domainName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseStackName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stackReference&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;baseStackName&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Output&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// ad hoc app env&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;adHocAppComponent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AdHocAppComponent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AdHocAppComponent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;vpcId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;assetsBucketName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;privateSubnets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;appSgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;albSgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;listenerArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;albDnsName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;serviceDiscoveryNamespaceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;rdsAddress&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;domainName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;baseStackName&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  CLI scaffolding
&lt;/h2&gt;

&lt;p&gt;CDK and Pulumi have some good options for how to scaffold a project.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pulumi has &lt;code&gt;pulumi new aws-typescript&lt;/code&gt; among lots of other options (run &lt;code&gt;pulumi new -l&lt;/code&gt; to see over 200 project types). I used this to create the library itself, the examples and the pulumi projects that I use in &lt;code&gt;django-step-by-step&lt;/code&gt; that consume the library.&lt;/li&gt;
&lt;li&gt;CDK has &lt;code&gt;projen&lt;/code&gt; CLI commands which can help set up either library code or project code&lt;/li&gt;
&lt;li&gt;The major benefits of these tools is setting up &lt;code&gt;tsconfig.json&lt;/code&gt; and &lt;code&gt;package.json&lt;/code&gt; correctly&lt;/li&gt;
&lt;li&gt;Terraform is so simple that it doesn't really need tooling for scaffolding&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Best practices
&lt;/h2&gt;

&lt;p&gt;For &lt;code&gt;terraform-aws-django&lt;/code&gt;, I tried to follow the recommendations from &lt;a href="https://www.terraform-best-practices.com/" rel="noopener noreferrer"&gt;terraform-best-practices.com&lt;/a&gt; which helped me a lot with things like consistent naming patterns and directory structures. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use the name &lt;code&gt;this&lt;/code&gt; for resources in a module where that resource is the only resource of its type&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CDK and Pulumi lend themselves to more nesting and abstractions because they can be written in more familiar programming languages with better abstractions, functions, loops, classes, etc., so there are some differences in directory structure of my libraries when comparing Terraform to both CDK and Pulumi.&lt;/p&gt;

&lt;p&gt;For Pulumi and CDK, I mostly tried to follow along with recommendations from their documentation and example projects. While working with Pulumi I struggled a bit with the concepts of &lt;code&gt;Inputs&lt;/code&gt;, &lt;code&gt;Outputs&lt;/code&gt;, &lt;code&gt;pulumi.interpolate&lt;/code&gt;, &lt;code&gt;apply()&lt;/code&gt;, &lt;code&gt;all()&lt;/code&gt; and the differences between &lt;code&gt;getX&lt;/code&gt; and &lt;code&gt;getXOutput&lt;/code&gt;. There is a little bit of a learning curve here, but the documentation and examples go a long way in showing how to do things the right way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment configuration
&lt;/h2&gt;

&lt;p&gt;Environment configuration allows for either a base or app stack to be configured with non-default values. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you may decide to start a new base environment but you want to provision a powerful database instance class and size. You would change this using environment configuration&lt;/li&gt;
&lt;li&gt;You might want to create an ad hoc app environment but you need it to include some special environment variables, you could set these in environment config.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the examples above, our IaC can optionally take environment configuration values that overwrite default values, or extend default values.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pulumi defines environment-specific config in files called &lt;code&gt;Pulumi.{env}.yaml&lt;/code&gt; (&lt;a href="https://www.pulumi.com/docs/intro/concepts/config/" rel="noopener noreferrer"&gt;Pulumi article on configuration&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Terraform uses &lt;code&gt;{env}.tfvars&lt;/code&gt; for this type of configuration&lt;/li&gt;
&lt;li&gt;CDK has several options for this type of configuration (&lt;code&gt;cdk.context.json&lt;/code&gt;, extending stack props, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For CDK I have been using &lt;code&gt;setContext&lt;/code&gt; and the &lt;code&gt;tryGetContext&lt;/code&gt; method:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;setContext&lt;/code&gt; needs to be set on the node before any child nodes are added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;baseStack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleAdHocBaseStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;baseStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;appStack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ExampleAdHocAppStack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stackName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;adHocAppEnvName&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;appStack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;adHocAppEnvConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the config objects are read from JSON files like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;adHocBaseEnvConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`src/examples/ad-hoc/base/config/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;adHocBaseEnvName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;adHocAppEnvConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`src/examples/ad-hoc/app/config/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;adHocAppEnvName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.json`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context can be used in constructs like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;extraEnvVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tryGetContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;config&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;extraEnvVars&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pulumi has similar functions for getting context values, here's an example of how I get extra environment variables for app environments using Pulumi's config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;    &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;EnvVar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;extraEnvVars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getObject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;EnvVar&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;extraEnvVars&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my &lt;code&gt;Pulumi.alpha.yaml&lt;/code&gt; file I have the &lt;code&gt;extraEnvVars&lt;/code&gt; set like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;aws:region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt;
  &lt;span class="na"&gt;extraEnvVars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;FOO&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BAR&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BIZ&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;BUZ&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I haven't done too much with configuration, but it seems like the right place to build out all of the dials and switches for optional settings in stack resources that you want people to be able to change in their ad hoc environments, or that you want to set per "production" environment (QA, stage, prod, etc.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Local development
&lt;/h2&gt;

&lt;p&gt;Using the Makefile targets in each library repo, my process for developing &lt;code&gt;c/m/c&lt;/code&gt;s involves making code changes followed by Makefile targets that preview/plan/diff against my AWS account, then running deploy/apply/up and waiting for things to finish deploying. Once I can validate that things are looking correct in my account, I run the destroy command and make sure that all of the resources are removed successfully. RDS instances can take up to 10 minutes to create, which means that the base stack takes some time to test. The app environment is able to be spun up quickly, but it can sometimes get stuck and take some time to delete services.&lt;/p&gt;

&lt;p&gt;Here are some sample times for deploying ad hoc stacks with CDK.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# CDK ad hoc base deployment time

 ✅  ExampleAdHocBaseStack (dev)

✨  Deployment time: 629.64s

# CDK ad hoc app deployment time

 ✅  ExampleAdHocAppStack (alpha)

✨  Deployment time: 126.62s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is an example of what the &lt;code&gt;pulumi preview&lt;/code&gt; commands shows for the ad-hoc base stack:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Pulumi preview
~/git/github/pulumi-aws-django$ pulumi -C examples/ad-hoc/base --stack dev preview
Previewing update (dev)

View Live: https://app.pulumi.com/briancaffey/ad-hoc-base/dev/previews/718625b2-48f5-4ef4-8ed4-9b2694fda64a

     Type                                                    Name                        Plan
 +   pulumi:pulumi:Stack                                     ad-hoc-base-dev             create
 +   └─ pulumi-contrib:components:AdHocBaseEnv               myAdHocEnv                  create
 +      ├─ pulumi-contrib:components:AlbResources            AlbResources                create
 +      │  ├─ aws:alb:TargetGroup                            DefaultTg                   create
 +      │  ├─ aws:alb:LoadBalancer                           LoadBalancer                create
 +      │  ├─ aws:alb:Listener                               HttpListener                create
 +      │  └─ aws:alb:Listener                               HttpsListener               create
 +      ├─ pulumi-contrib:components:BastionHostResources    BastionHostResources        create
 +      │  ├─ aws:iam:Role                                   BastionHostRole             create
 +      │  ├─ aws:iam:RolePolicy                             BastionHostPolicy           create
 +      │  ├─ aws:iam:InstanceProfile                        BastionHostInstanceProfile  create
 +      │  └─ aws:ec2:Instance                               BastionHostInstance         create
 +      ├─ pulumi-contrib:components:RdsResources            RdsResources                create
 +      │  ├─ aws:rds:SubnetGroup                            DbSubnetGroup               create
 +      │  ├─ aws:ec2:SecurityGroup                          RdsSecurityGroup            create
 +      │  └─ aws:rds:Instance                               DbInstance                  create
 +      ├─ pulumi-contrib:components:SecurityGroupResources  SecurityGroupResources      create
 +      │  ├─ aws:ec2:SecurityGroup                          AlbSecurityGroup            create
 +      │  └─ aws:ec2:SecurityGroup                          AppSecurityGroup            create
 +      ├─ aws:s3:Bucket                                     assetsBucket                create
 +      ├─ awsx:ec2:Vpc                                      dev                         create
 +      │  └─ aws:ec2:Vpc                                    dev                         create
 +      │     ├─ aws:ec2:InternetGateway                     dev                         create
 +      │     ├─ aws:ec2:Subnet                              dev-private-1               create
 +      │     │  └─ aws:ec2:RouteTable                       dev-private-1               create
 +      │     │     ├─ aws:ec2:RouteTableAssociation         dev-private-1               create
 +      │     │     └─ aws:ec2:Route                         dev-private-1               create
 +      │     ├─ aws:ec2:Subnet                              dev-private-2               create
 +      │     │  └─ aws:ec2:RouteTable                       dev-private-2               create
 +      │     │     ├─ aws:ec2:RouteTableAssociation         dev-private-2               create
 +      │     │     └─ aws:ec2:Route                         dev-private-2               create
 +      │     ├─ aws:ec2:Subnet                              dev-public-1                create
 +      │     │  ├─ aws:ec2:RouteTable                       dev-public-1                create
 +      │     │  │  ├─ aws:ec2:RouteTableAssociation         dev-public-1                create
 +      │     │  │  └─ aws:ec2:Route                         dev-public-1                create
 +      │     │  ├─ aws:ec2:Eip                              dev-1                       create
 +      │     │  └─ aws:ec2:NatGateway                       dev-1                       create
 +      │     └─ aws:ec2:Subnet                              dev-public-2                create
 +      │        ├─ aws:ec2:RouteTable                       dev-public-2                create
 +      │        │  ├─ aws:ec2:RouteTableAssociation         dev-public-2                create
 +      │        │  └─ aws:ec2:Route                         dev-public-2                create
 +      │        ├─ aws:ec2:Eip                              dev-2                       create
 +      │        └─ aws:ec2:NatGateway                       dev-2                       create
 +      └─ aws:servicediscovery:PrivateDnsNamespace          PrivateDnsNamespace         create


Outputs:
    albDnsName                 : output&amp;lt;string&amp;gt;
    albSgId                    : output&amp;lt;string&amp;gt;
    appSgId                    : output&amp;lt;string&amp;gt;
    assetsBucketName           : output&amp;lt;string&amp;gt;
    baseStackName              : "dev"
    bastionHostInstanceId      : output&amp;lt;string&amp;gt;
    domainName                 : "example.com"
    listenerArn                : output&amp;lt;string&amp;gt;
    privateSubnetIds           : output&amp;lt;string&amp;gt;
    rdsAddress                 : output&amp;lt;string&amp;gt;
    serviceDiscoveryNamespaceId: output&amp;lt;string&amp;gt;
    vpcId                      : output&amp;lt;string&amp;gt;

Resources:
    + 44 to create
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running infrastructure pipelines in GitHub Actions
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;I don't currently have GitHub Actions working for all tools in all environments, this part is still a WIP but is working at a basic level. Another item for the backlog!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;.github/workflows&lt;/code&gt; directory of the &lt;code&gt;django-step-by-step&lt;/code&gt; repo, I will have the following &lt;code&gt;2 * 2 * 2 * 3 = 24&lt;/code&gt; pipelines for running infrastructure as code pipelines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;{&lt;/span&gt;ad_hoc,prod&lt;span class="o"&gt;}&lt;/span&gt;_&lt;span class="o"&gt;{&lt;/span&gt;base,app&lt;span class="o"&gt;}&lt;/span&gt;_&lt;span class="o"&gt;{&lt;/span&gt;create_update,destroy&lt;span class="o"&gt;}&lt;/span&gt;_&lt;span class="o"&gt;{&lt;/span&gt;cdk,terraform,pulumi&lt;span class="o"&gt;}&lt;/span&gt;.yml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;For CDK I'm using CDK CLI commands&lt;/li&gt;
&lt;li&gt;For Terraform I'm also using terraform CLI commands&lt;/li&gt;
&lt;li&gt;For Pulumi I'm using the official Pulumi GitHub Action&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Pulumi has &lt;a href="https://www.pulumi.com/docs/guides/continuous-delivery/github-actions/" rel="noopener noreferrer"&gt;a great article&lt;/a&gt; about how to use their official GitHub Action. This action calls the Pulumi CLI under the hood with all of the correct flags.&lt;/p&gt;

&lt;p&gt;The general pattern that all of these pipelines use is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do a synth/plan/preview, and upload the synth/plan/preview file to an artifact&lt;/li&gt;
&lt;li&gt;Pause and wait on manual review of the planned changes&lt;/li&gt;
&lt;li&gt;download the artifact and run deploy/apply/up against it, or optionally cancel the operation if the changes you see in the GitHub Actions pipeline logs are not what you expected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do this by having two jobs in each GitHub Action: one for synth/plan/preview and one for deploy/apply/up.&lt;/p&gt;

&lt;p&gt;The job for deploy/apply/up includes an &lt;code&gt;environment&lt;/code&gt; that is configured in GitHub to be a protected environment that requires approvals. Even if you are the only approver (which I am on this project), it is the easiest and safest way preview infrastructure changes before they happen. If you see something in the plan and it isn't what you wanted to change, you cancel the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Application deployments
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;There are two GitHub Actions pipelines for deploying the frontend and the backend. Both of these pipelines run bash scripts that call AWS CLI commands to perform rolling updates on all of the services used in the application (frontend, API, workers, scheduler)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The backend deployment script runs database migrations, the &lt;code&gt;collectstatic&lt;/code&gt; command and any other commands needed to run before the rolling update starts (clearing the cache, loading fixtures, etc.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;What is important to note here is that application deployments are not dependent on the IaC tool we use. Since we are tagging things consistently across CDK, Terraform and Pulumi, we can look up resources by tag rather than getting "outputs" of the app stacks.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Interacting with AWS via IaC
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;CDK interacts directly with CloudFormation (and custom resources which allow for running arbitrary SDK calls and lambda functions) and provides &lt;a href="https://docs.aws.amazon.com/cdk/v2/guide/constructs.html" rel="noopener noreferrer"&gt;L1, L2 and L3 constructs&lt;/a&gt; which offer different levels of abstraction over CloudFormation.&lt;/li&gt;
&lt;li&gt;Terraform has the &lt;a href="https://registry.terraform.io/providers/hashicorp/aws/latest/docs" rel="noopener noreferrer"&gt;AWS Provider&lt;/a&gt; and the &lt;a href="https://registry.terraform.io/namespaces/terraform-aws-modules" rel="noopener noreferrer"&gt;&lt;code&gt;terraform-aws-modules&lt;/code&gt;&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Pulumi has AWS Classic (&lt;code&gt;@pulumi/aws&lt;/code&gt;) &lt;strong&gt;provider&lt;/strong&gt; and &lt;code&gt;AWSx&lt;/code&gt; (&lt;a href="https://www.pulumi.com/registry/packages/awsx/" rel="noopener noreferrer"&gt;Crosswalk for Pulumi&lt;/a&gt;) &lt;strong&gt;library&lt;/strong&gt; and &lt;code&gt;aws_native&lt;/code&gt; &lt;strong&gt;provider&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;aws_native&lt;/code&gt; "manages and provisions resources using the AWS Cloud Control API, which typically supports new AWS features on the day of launch."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;code&gt;aws_native&lt;/code&gt; looks like a really interesting option, but it is currently in public preview so I have not decided to use it. I am using the AWSx library only for my VPC and associated resources, everything else uses the AWS Classic provider.&lt;/p&gt;

&lt;p&gt;For CDK I use mostly L2 constructs and some L1 constructs.&lt;/p&gt;

&lt;p&gt;Fot Terraform I use the VPC from the &lt;code&gt;terraform-aws-modules&lt;/code&gt;, and everything else uses the AWS Terraform Provider.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not put in IaC
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;ECR (Elastic Container Registry)&lt;/li&gt;
&lt;li&gt;ACM (Amazon Certificate Manager)&lt;/li&gt;
&lt;li&gt;(Roles used for deployments)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I created the Elastic Container Registry &lt;code&gt;backend&lt;/code&gt; and &lt;code&gt;frontend&lt;/code&gt; repos manually in the AWS Console. I also manually requested an ACM certificate for &lt;code&gt;*.mydomain.com&lt;/code&gt; for the domain that I use for testing that I purchased through Route53 domains.&lt;/p&gt;

&lt;p&gt;I currently am using another less-than best practice of using Administrative Credentials stored in GitHub secrets. The better approach here is to make roles for different pipelines and use OIDC to authenticate instead of storing credentials. This is another good item for the backlog.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tagging
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Terraform and CDK both make it easy to automatically tag all resources in a stack&lt;/li&gt;
&lt;li&gt;It is possible to do this in Pulumi, but you need to write a little bit of code.&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.pulumi.com/blog/automatically-enforcing-aws-resource-tagging-policies/" rel="noopener noreferrer"&gt;https://www.pulumi.com/blog/automatically-enforcing-aws-resource-tagging-policies/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/joeduffy/aws-tags-example/tree/master/autotag-ts" rel="noopener noreferrer"&gt;https://github.com/joeduffy/aws-tags-example/tree/master/autotag-ts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Tagging is important since I look up resources by tag in GitHub Actions pipelines (for example, the Bastion Host is looked up by tag)&lt;/li&gt;
&lt;li&gt;Automatically tagging resources works through &lt;a href="https://www.pulumi.com/docs/intro/vs/terraform/" rel="noopener noreferrer"&gt;stack transformations&lt;/a&gt; are unique to Pulumi&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Smoke checking application environments
&lt;/h2&gt;

&lt;p&gt;Here's the list of things I check when standing up an application environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[x] Run the init/tsc, synth/plan/preview and deploy/apply/up commands successfully&lt;/li&gt;
&lt;li&gt;[x] Access the bastion host (&lt;code&gt;make aws-ssm-start-session&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Run ECSExec to access a shell in a backend container (&lt;code&gt;make aws-ecs-exec&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Test database connectivity (&lt;code&gt;python manage.py showmigrations&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Run the migrations (&lt;code&gt;python manage.py migrate&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Run collectstatic (&lt;code&gt;python manage.py collectstatic&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Visit the site (&lt;code&gt;alpha.example.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;[x] Publish a blog post&lt;/li&gt;
&lt;li&gt;[x] Publish a blog post with an image&lt;/li&gt;
&lt;li&gt;[x] Check celery worker logs for successfully complete scheduled tasks&lt;/li&gt;
&lt;li&gt;[x] Trigger an autoscaling event by running k6 load tests against an environment&lt;/li&gt;
&lt;li&gt;[x] Optionally deploy another backend or frontend image tag using the GitHub Actions pipelines for backend and frontend updates&lt;/li&gt;
&lt;li&gt;[x] Destroy the app stack&lt;/li&gt;
&lt;li&gt;[x] Destroy the base stack&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Backlog and next steps
&lt;/h2&gt;

&lt;p&gt;Here are some of the next things I'll be working on in these project, roughly in order of importance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Introduce manual approvals in GitHub Actions for all deployments and allow for the previewing or "planning" before proceeding with an live operations in infrastructure pipelines&lt;/li&gt;
&lt;li&gt;Switch to using OIDC for AWS authentication from GitHub Actions and remove AWS secrets from GitHub&lt;/li&gt;
&lt;li&gt;Show how to do account isolation (different accounts for prod vs pre-prod environments)&lt;/li&gt;
&lt;li&gt;GitHub Actions deployment pipeline for publishing &lt;code&gt;pulumi-aws-django&lt;/code&gt; package&lt;/li&gt;
&lt;li&gt;Complete all GitHub Action deployment pipelines for base and app stacks (both ad hoc and prod)&lt;/li&gt;
&lt;li&gt;For Pulumi and Terraform, use a Secrets Manager secret for the database instead of hardcoding it. Use the &lt;code&gt;random&lt;/code&gt; functions to do this&lt;/li&gt;
&lt;li&gt;Refactor GitHub Actions and make them reusable across different projects&lt;/li&gt;
&lt;li&gt;Writing tests for Pulumi and CDK. Figure out how to write tests for Terraform modules&lt;/li&gt;
&lt;li&gt;Use graviton instances and have the option to select between different architectures&lt;/li&gt;
&lt;li&gt;Standardize all resources names across CDK, Terraform and Pulumi&lt;/li&gt;
&lt;li&gt;The Pulumi components that define the resources associated with each ECS service are not very dry&lt;/li&gt;
&lt;li&gt;Interfaces could be constructed with inheritance (base set of properties that is extended for different types of services)&lt;/li&gt;
&lt;li&gt;Fix the CDK issue with priority rule on ALB listeners. I need to used a custom resource for this which is currently a WIP. Terraform and Pulumi look up the next highest listener rule priority under the hood, so you are not required to provide it, but CDK requires it, which means that you can't do ad hoc environments in CDK without a custom resource that looks up what the next available priority number is.&lt;/li&gt;
&lt;li&gt;Make all three of the libraries less opinionated. For example, the celery worker and scheduler should be optional and the frontend component should also be optional&lt;/li&gt;
&lt;li&gt;experiment with using a frontend with SSR. This is supported by Quasar, the framework I'm currently using to build my frontend SPA site&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to get involved or help with any of the above, please let me know!&lt;/p&gt;

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

&lt;p&gt;I first started out with IaC following this project &lt;a href="https://github.com/aws-samples/ecs-refarch-cloudformation" rel="noopener noreferrer"&gt;aws-samples/ecs-refarch-cloudformation&lt;/a&gt; (which is pretty old at this point) and wrote a lot of CloudFormation by hand. The pain of doing that lead me to explore the CDK with Python. I learned TypeScript by rewriting the Python CDK code I wrote in TypeScript. I later worked with a team that was more experienced in Terraform and learned how to use that. I feel like Pulumi takes the best of the two tools and has a really great developer experience. There is a little bit of a learning curve with Pulumi, and you give up some of the simplicity of Terraform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing
&lt;/h2&gt;

&lt;p&gt;Thank you for reading to the end! I have posted the article on the following channels, please like and share the article and follow me wherever you are active. Also please share your feedback!&lt;/p&gt;

&lt;p&gt;(links pending)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://briancaffey.github.io/2023/01/07/i-deployed-the-same-containerized-serverless-django-app-with-aws-cdk-terraform-and-pulumi" rel="noopener noreferrer"&gt;my personal blog (briancaffey.github.io)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;DEV.to&lt;/li&gt;
&lt;li&gt;Ops Community&lt;/li&gt;
&lt;li&gt;Medium&lt;/li&gt;
&lt;li&gt;Substack&lt;/li&gt;
&lt;li&gt;My MailChimp mailing list&lt;/li&gt;
&lt;li&gt;Hacker News&lt;/li&gt;
&lt;li&gt;Twitter&lt;/li&gt;
&lt;li&gt;LinkedIn&lt;/li&gt;
&lt;li&gt;Facebook&lt;/li&gt;
&lt;li&gt;HashNode&lt;/li&gt;
&lt;li&gt;Hackernoon&lt;/li&gt;
&lt;li&gt;Reddit (r/aws, r/Terraform, r/pulumi, r/django, r/devops)&lt;/li&gt;
&lt;li&gt;Terraform Forum&lt;/li&gt;
&lt;li&gt;Pulumi Slack channel&lt;/li&gt;
&lt;li&gt;CDK.dev Slack channel&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>cdk</category>
      <category>terraform</category>
      <category>pulumi</category>
    </item>
    <item>
      <title>Redis Light, Green Light</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Mon, 29 Aug 2022 02:44:40 +0000</pubDate>
      <link>https://dev.to/briancaffey/redis-light-green-light-19c0</link>
      <guid>https://dev.to/briancaffey/redis-light-green-light-19c0</guid>
      <description>&lt;h3&gt;
  
  
  Overview of My Submission
&lt;/h3&gt;

&lt;p&gt;I recreated the game "Red Light, Green Light" using Python, TypeScript and Redis!&lt;/p&gt;

&lt;p&gt;One day in early August I was browsing DEV while re-watching MrBeast's Squid Game recreation video in the background when I came across the Redis Hackathon article. Then I got a crazy, random idea: &lt;strong&gt;Redis Light, Green Light&lt;/strong&gt;! &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3rqy7eqz40nqk1e04jj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fm3rqy7eqz40nqk1e04jj.png" alt="Redis Light, Green Light" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I became determined to create my own online, real-time, multi-player version of Red Light, Green Light powered by Redis and submit it to the Wacky Wildcard project category for my chance to win the hackathon!&lt;/p&gt;

&lt;p&gt;I used my favorite languages and frameworks for rapid prototyping: Python with Flask to power the backend and TypeScript with the Nuxt.js framework to build the frontend components for my game.&lt;/p&gt;

&lt;p&gt;For real-time communication I added the &lt;code&gt;Flask-SocketIO&lt;/code&gt; library to my Flask app and the &lt;code&gt;socket.io-client&lt;/code&gt; library to my Nuxt app. I also added celery for scheduling and processing async tasks. Redis was used as the message queue for websocket messages and it was also used as the broker for celery tasks.&lt;/p&gt;

&lt;p&gt;This was my first project working with Redis Stack and Redis OM and I really liked using these tools. I stored most of my data in hashes, and the Redis OM library is perfect for using this data type. I also used Redis streams for the first time which was a lot of fun.&lt;/p&gt;

&lt;p&gt;The backend application services include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flask server (for API endpoints and Websocket handlers)&lt;/li&gt;
&lt;li&gt;Celery beat task scheduler (for scheduling tasks to change the light color in each room)&lt;/li&gt;
&lt;li&gt;Celery worker (to change the light color for a room and to update that players in that room via Websocket)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0egpm1igfqyon5kjtujv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0egpm1igfqyon5kjtujv.png" alt="Project Diagram" width="800" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Please check out the video below for more details about how the project works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Submission Category
&lt;/h3&gt;

&lt;p&gt;Wacky Wildcards&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis Light, Green Light YouTube Video
&lt;/h3&gt;

&lt;p&gt;&lt;iframe width="710" height="399" src="https://www.youtube.com/embed/BoalZKmgoEU"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Language Used
&lt;/h3&gt;

&lt;p&gt;Python. Honorable mention for JavaScript.&lt;/p&gt;

&lt;h3&gt;
  
  
  Link to Code
&lt;/h3&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fassets.dev.to%2Fassets%2Fgithub-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/briancaffey" rel="noopener noreferrer"&gt;
        briancaffey
      &lt;/a&gt; / &lt;a href="https://github.com/briancaffey/redis-light-green-light-dev-to-hackathon" rel="noopener noreferrer"&gt;
        redis-light-green-light-dev-to-hackathon
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      My submission for the Redis Hackathon on DEV! "Red Light, Green Light" built with Python, JavaScript and Redis
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Redis Light, Green Light&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;This project is an online, multiplayer implementation of "Red Light, Green Light" from Squid Game built with Python, Javascript and Redis. This is my submission for the 2022 &lt;a href="https://dev.to/devteam/announcing-the-redis-hackathon-on-dev-3248" rel="nofollow"&gt;Redis Hackathon on DEV&lt;/a&gt;!&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Gameplay&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/briancaffey/redis-light-green-light-dev-to-hackathon/images/gameplay.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fbriancaffey%2Fredis-light-green-light-dev-to-hackathon%2Fimages%2Fgameplay.png" alt="Redis Light, Green Light Gameplay"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Game event log built with redis streams&lt;/h3&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/briancaffey/redis-light-green-light-dev-to-hackathon/images/events.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fbriancaffey%2Fredis-light-green-light-dev-to-hackathon%2Fimages%2Fevents.png" alt="Redis Stream data"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Architecture Overview&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/briancaffey/redis-light-green-light-dev-to-hackathon/images/rlgl.drawio.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.com%2Fbriancaffey%2Fredis-light-green-light-dev-to-hackathon%2Fimages%2Frlgl.drawio.png" alt="Project Architecture Diagram"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Code Overview with &lt;code&gt;cloc&lt;/code&gt; (count lines of code)&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;make cloc

github.com/AlDanial/cloc v 1.94  T=0.03 s (1102.7 files/s, 67098.7 lines/s)
-------------------------------------------------------------------------------
Language                     files          blank        comment           code
-------------------------------------------------------------------------------
Vuejs Component                 13            104             14            528
Python                           3            178            104            410
Markdown                         5            139              0            272
YAML                             3             10              0             83
make                             1             10              2             34
SVG                              2              0              0             22
TypeScript                       1              1              1             22
CSS                              1              4              0             18
JavaScript                       1              0              1             18
Text                             2              0              0             13
Dockerfile                       1              8              0             12
-------------------------------------------------------------------------------
SUM:                            33            454            122           1432
-------------------------------------------------------------------------------
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Overview video&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;Here's a short video that explains the project and how it uses…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/briancaffey/redis-light-green-light-dev-to-hackathon" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h3&gt;
  
  
  Additional Resources / Info
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkmgiz8wtq484rn6dmfc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwkmgiz8wtq484rn6dmfc.png" alt="Redis Light, Green Light gameplay" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fujjvd5vjqcotjxn2jcms.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fujjvd5vjqcotjxn2jcms.png" alt="Redis Streams" width="800" height="738"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Check out &lt;a href="https://redis.io/docs/stack/get-started/clients/#high-level-client-libraries" rel="noopener noreferrer"&gt;Redis OM&lt;/a&gt;, client libraries for working with Redis as a multi-model database.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Use &lt;a href="https://redis.info/redisinsight" rel="noopener noreferrer"&gt;RedisInsight&lt;/a&gt; to visualize your data in Redis.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Sign up for a &lt;a href="https://redis.info/try-free-dev-to" rel="noopener noreferrer"&gt;free Redis database&lt;/a&gt;.&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>redishackathon</category>
      <category>redis</category>
      <category>flask</category>
      <category>socketio</category>
    </item>
    <item>
      <title>Setting up ad hoc development environments for Django applications with AWS ECS, Terraform and GitHub Actions</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Sat, 11 Jun 2022 16:20:47 +0000</pubDate>
      <link>https://dev.to/briancaffey/setting-up-ad-hoc-development-environments-for-django-applications-with-aws-ecs-terraform-and-github-actions-4abh</link>
      <guid>https://dev.to/briancaffey/setting-up-ad-hoc-development-environments-for-django-applications-with-aws-ecs-terraform-and-github-actions-4abh</guid>
      <description>&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;This article will show how software development teams can build on-demand instances of a web application project for dog-food testing, quality review, internal and external demos and other use cases that require short-lived but feature-complete environments. It will focus on the technical implementation of building ad hoc environments using a specific set of tools (including AWS ECS, Terraform and GitHub Actions). I will also be giving context on high-level implementation decisions based on what I think are best practices guided by the &lt;a href="https://12factor.net/" rel="noopener noreferrer"&gt;12-Factor Application methodology&lt;/a&gt;. If any of this interests you, please have a read and let me know what you think in the comments on the outlets where I'll be sharing this article (links at the end).&lt;/p&gt;

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

&lt;p&gt;This article references three open-source code repositories on GitHub.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://github.com/briancaffey/django-step-by-step" rel="noopener noreferrer"&gt;django-step-by-step&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;this repo contains an example microblogging application called μblog built with Django&lt;/li&gt;
&lt;li&gt;the same application is implemented as a traditional Model Template View (MTV) site, a decoupled REST API and Javascript web application and a GraphQL API&lt;/li&gt;
&lt;li&gt;it is a monorepo that also includes a frontend Vue.js application, CI/CD pipelines, a VuePress documentation site as well as tooling and instructions for settings up a local development environments (both with and without docker)&lt;/li&gt;
&lt;li&gt;it includes a complete set of GitHub Action examples for automating the processes of creating, updating and destroying ad hoc environments that will be an important part of what is covered in this article&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;a href="https://github.com/briancaffey/terraform-aws-django" rel="noopener noreferrer"&gt;terraform-aws-django&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a collection of modules for running Django applications on AWS using Terraform&lt;/li&gt;
&lt;li&gt;one of the submodules can be used for creating ad hoc environments which will be what we use to create ad hoc environments&lt;/li&gt;
&lt;li&gt;this module has been published to Terraform Registry and is used in the &lt;code&gt;terraform/live/ad-hoc&lt;/code&gt; directory of the &lt;code&gt;django-step-by-step&lt;/code&gt; repo&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;a href="https://github.com/briancaffey/terraform-aws-ad-hoc-environments" rel="noopener noreferrer"&gt;terraform-aws-ad-hoc-environments&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a Terraform module that provides shared infrastructure used by ad hoc environments (including VPC, RDS instance, bastion host, security groups and IAM roles, etc.)&lt;/li&gt;
&lt;li&gt;this module has also been published to Terraform Registry and is also used in &lt;code&gt;django-step-by-step&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;this module is designed to be used with the &lt;code&gt;terraform-aws-django&lt;/code&gt; Terraform module&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Assumptions
&lt;/h2&gt;

&lt;p&gt;There are all sorts of applications, and all sort of engineering teams. For some context on what I'm describing in this article, here are some basic assumptions that I'm making about the type of engineering team and software application product that would be a good fit for this type of development workflow.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;engineering team is composed of a backend team, a frontend team, a devops team and works closely with a product team&lt;/li&gt;
&lt;li&gt;backend team primarily develops a REST API&lt;/li&gt;
&lt;li&gt;frontend team develops a JavaScript SPA (frontend website)&lt;/li&gt;
&lt;li&gt;SPA consumes backend REST API&lt;/li&gt;
&lt;li&gt;product team frequently needs to demo applications to prospective clients&lt;/li&gt;
&lt;li&gt;development teams don't have deep expertise in infrastructure, containers, CI/CD or automation&lt;/li&gt;
&lt;li&gt;devops team has been tasked with building automation that will allow anyone on the team to quickly spin up a complete environment for testing and demoing purposes within minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here are assumptions about specific tools and technologies used at the company:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend is a REST API developed with Django and a Postgres database&lt;/li&gt;
&lt;li&gt;backend is packaged into a docker container&lt;/li&gt;
&lt;li&gt;frontend is also packaged into a docker container using multi-stage builds and NGINX&lt;/li&gt;
&lt;li&gt;frontend does not require any build-time configuration (all configuration needed by frontend is fetched from backend)&lt;/li&gt;
&lt;li&gt;backend application's configuration is driven by plain-text environment variables at run-time&lt;/li&gt;
&lt;li&gt;engineering team uses AWS&lt;/li&gt;
&lt;li&gt;automation pipeline exists for building, tagging and pushing backend and frontend container images to an ECR repository&lt;/li&gt;
&lt;li&gt;devops team uses AWS ECS for running containerized workloads&lt;/li&gt;
&lt;li&gt;devops team uses Terraform for provisioning infrastructure&lt;/li&gt;
&lt;li&gt;devops team uses GitHub Actions for building automation pipelines&lt;/li&gt;
&lt;li&gt;team is somewhat cost-conscious&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What are ad hoc environments?
&lt;/h2&gt;

&lt;p&gt;Ad hoc environments are short-lived environments that are designed to be used for testing a specific set of features or for demoing a specific application configuration in an isolated environment. It is intended to be a functional duplicate of the main production environment. An ad hoc environment is the first cloud environment that the application code will be deployed to after a developer has been working on it in a local development environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trade-offs to make when designing ad hoc environment infrastructure and automation
&lt;/h2&gt;

&lt;p&gt;Now that we have a sense of what we are building and the team we are working with, let's think about the high-level trade-offs that we will face as we build a solution for providing on-demand ad hoc environments. When building infrastructure and workflows for ad hoc environments, there are a few things to solve for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;simplicity of the end-user interface and process for requesting an ad hoc environment&lt;/li&gt;
&lt;li&gt;startup speed&lt;/li&gt;
&lt;li&gt;total cost of ownership&lt;/li&gt;
&lt;li&gt;degree of similarity to production environments&lt;/li&gt;
&lt;li&gt;shared vs isolated resources&lt;/li&gt;
&lt;li&gt;automation complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's look at these items by considering how we can set up the main infrastructure components that will be used to run our ad hoc application environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Relational Databases
&lt;/h3&gt;

&lt;p&gt;Startup speed can be measured by the time between when an environment is requested and when that environment can be used by whoever requested it. In this period of time, an automation pipeline may do some of the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;run &lt;code&gt;terraform init&lt;/code&gt;, &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt; to build infrastructure&lt;/li&gt;
&lt;li&gt;run scripts to prepare the application such as database migrations&lt;/li&gt;
&lt;li&gt;seeding initial sample data with a script or database dump&lt;/li&gt;
&lt;li&gt;message the user with information about the environment (URLs, commands for accessing an interactive shell, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;RDS instances can take a long time to create relative to other AWS resources such as S3 buckets and IAM roles. RDS instances are also more costly than other resources. We could use a single, shared RDS instance placed in a private subnet of a shared VPC. Each ad hoc environment could use a different named database in the RDS instance in the form &lt;code&gt;{ad-hoc-env-name}-db&lt;/code&gt;. Using one RDS instance per ad hoc environment would be slow to startup and tear down and also costly if there are many developers using ad hoc environments simultaneously.&lt;/p&gt;

&lt;p&gt;If we choose to isolate the application's relational database at the database level (and not the RDS instance level), then we will need our automation workflow to create a database per ad hoc environment.&lt;/p&gt;

&lt;p&gt;Let's spin up a simple example to illustrate how this would would work.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A developer is working on &lt;code&gt;feature-abc&lt;/code&gt; that involves a significant refactor of the data model.&lt;/li&gt;
&lt;li&gt;The developer decides to spin up an ad hoc environment called &lt;code&gt;feature-abc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Our automation will need to create a database in the RDS instance called &lt;code&gt;feature-abc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We can configure a bastion host with &lt;code&gt;psql&lt;/code&gt; installed that has network and security group access to our RDS instance, and we can give our GitHub Actions an SSH key that can be used to run &lt;code&gt;createdb&lt;/code&gt; over SSH.&lt;/li&gt;
&lt;li&gt;The automation also runs database migrations once the application has started, and we can view the logs of the database migration to check for any irregularities or other issues.&lt;/li&gt;
&lt;li&gt;This will give the developer and the rest of team confidence that the promoting &lt;code&gt;feature-abc&lt;/code&gt; to the next pre-production environments will not have any errors.&lt;/li&gt;
&lt;li&gt;The developer may even choose to load a SQL dump of the next pre-production environment into their &lt;code&gt;feature-abc&lt;/code&gt; database get even more confidence that there will be no data integrity errors.&lt;/li&gt;
&lt;li&gt;When the developer's PR is merged and approved, the ad hoc environment &lt;code&gt;feature-abc&lt;/code&gt; can be destroyed, including the &lt;code&gt;feature-abc&lt;/code&gt; database in the shared RDS instance.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With this approach we won't incur the costs of multiple RDS instances. Ad hoc environments will start up faster because an RDS instance per environment is not required. We do have slightly less resource isolation, and we need to introduce a bastion host, but I consider this an acceptable trade-off.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis (key-value database)
&lt;/h3&gt;

&lt;p&gt;Redis is another database used in the application and it plays a few different roles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;primarily, it is a caching layer that can cache request responses to reduce load on the database and speed up our application&lt;/li&gt;
&lt;li&gt;it is a message broker for our async task workers (celery)&lt;/li&gt;
&lt;li&gt;it can be used as a backend for other 3rd party Django apps that our main application may need to use (such as django-constance, cache-ops, django-channels, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AWS offers a managed Redis service called ElastiCache. Redis running on an ElastiCache instance can do database isolation similar to how Postgres running on RDS can do database isolation as we discussed previously, but there are some key differences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;redis databases are numbered, not named&lt;/li&gt;
&lt;li&gt;the backend application uses isolated numbered databases for the different 3rd party apps that I just mentioned (for example: celery can use database &lt;code&gt;0&lt;/code&gt;,  API caching layer can use database &lt;code&gt;1&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This makes it difficult to use a single ElastiCache instance for our ad hoc environments since we would need to figure out which numbered database to assign to a specific role for each ad hoc environment (e.g. how do we know which numbered database to use for the API caching for the &lt;code&gt;feature-abc&lt;/code&gt; ad hoc environment).&lt;/p&gt;

&lt;p&gt;So how can we approach providing isolated redis instances for multiple ad hoc environments? Spoiler: my solution is to run redis as a stateful service in ECS. Before we dig into how to do this, we need to talk about another important part of our application: compute.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compute
&lt;/h3&gt;

&lt;p&gt;Our backend application is composed of a few different services that all share the same code base. In other words, our backend's services uses the same docker image but run different processes for each component:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;gunicorn for the core API&lt;/li&gt;
&lt;li&gt;celery for the task workers&lt;/li&gt;
&lt;li&gt;celerybeat for task scheduling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If our application used websockets, we could have another service that runs an asgi server process (like daphne or uvicorn).&lt;/p&gt;

&lt;p&gt;Since our backend application is packaged into a container and we are using AWS as our cloud provider, ECS is a great choice for running our backend services. ECS is a container orchestration tool that I usually describe as a nice middle ground between docker swarm and Kubernetes. Simply put, it is a flexible option for running our containerized services that make up our backend application.&lt;/p&gt;

&lt;p&gt;With ECS you can choose to run containers directly on EC2 instances that you manage, or you can run containers using Fargate. Fargate is a serverless compute option that takes care of managing both the underlying "computer" and operating system that run our application's containers. All of our backend dependencies are defined in our Dockerfile, so we do not to maintain or update the underlying operating system that runs our containers -- AWS handles all of this for us. To use Fargate, we simply tell AWS which containers to run and how much CPU and memory to use in the ECS Task that runs the containers. To scale our app horizontally, the ECS service that managed ECS tasks simply increases the number of tasks that run.&lt;/p&gt;

&lt;p&gt;Since we are going to use the Fargate launch type for our ECS Tasks, let's talk about the ergonomics of these serverless compute instances compared to running our services directly on an EC2 instances.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;We can't SSH into Fargate compute instances. We can instead use AWS Systems Manager and EcsExec to open an interactive shell in a running backend container. This can be useful for developers who might need to run a management command or access an interactive Django shell to verify behavior in their ad hoc environment.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We can't simply change code on the server and restart services. This can sometimes be a useful pattern for debugging something that can only be tested on a cloud environment (e.g. something that can't easily be reproduced on your local machine), so this requires that developers push new images to their backend services for every change they want to see reflected on their ad hoc environment. Later on I'll discuss how we can provide tooling for developers to quickly update the image used in their backend services.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With AWS Fargate, you will pay more than you would for a comparable amount of CPU and memory on EC2 instances. Similar to EC2 spot instances, Fargate offers interruptable instances called Fargate Spot which costs significantly less than regular Fargate instances. Fargate spot is appropriate for our ad hoc environments since ad hoc environments are non-critical workloads. In the event that a Fargate spot instance is interrupted, the ECS service will automatically launch another Fargate task to replace the task that was stopped.&lt;/p&gt;

&lt;p&gt;In my opinion, ECS with Fargate is ideal for running the stateless services that make up our backend application. In terms of parity with our production environment, we can keep almost everything the same, except use regular Fargate instances instead of Fargate spot instances.&lt;/p&gt;

&lt;h3&gt;
  
  
  Redis, revisited
&lt;/h3&gt;

&lt;p&gt;We can run redis as an ECS service instead of using ElastiCache. In order to do this, we will need our backend services (gunicorn, celery and celerybeat) to be able to communicate with a fourth ECS service that will be running redis (using an official redis image from Docker Hub, or a redis image that we define in ECR).&lt;/p&gt;

&lt;p&gt;By default, &lt;strong&gt;there is no way for our backend services to know how to communicate with any other service in our ECS cluster&lt;/strong&gt;. If you have used docker-compose, you may know that you can use the service name &lt;code&gt;redis&lt;/code&gt; in a backend service to easily communicate with a redis service called &lt;code&gt;redis&lt;/code&gt;. This networking convenience is not available to use out of the box with ECS. To achieve this in AWS, we need some way to manage a unique ad hoc environment-specific Route 53 DNS record that points to the private IP of the Fargate task that is running redis in an ECS cluster for a given ad hoc environment. Such a service exists in AWS and it is called Cloud Map. Cloud Map offers service discovery so that our backend services can make network calls to a static DNS address that will reliably point to the correct private IP of the ECS task running the redis container.&lt;/p&gt;

&lt;p&gt;We can define a service discovery namespace (which will essentially be a top level domain, or TLD) that all of our ad hoc environments can share. Let's assume this namespace is called &lt;code&gt;ad-hoc&lt;/code&gt;. Each ad hoc environment can then define a service discovery service in the shared namespace for redis that is called &lt;code&gt;{ad-hoc-env-name}-redis&lt;/code&gt;. This way, we can have a reliable address that we can configure as an environment for our backend that will look like this: &lt;code&gt;redis://{ad-hoc-env-name}-redis.ad-hoc:6379/0&lt;/code&gt;. &lt;code&gt;{ad-hoc-env-name-redis}.ad-hoc&lt;/code&gt; will be the hostname of the redis service, and Route 53 will create records that point to &lt;code&gt;{ad-hoc-env-name}-redis.ad-hoc&lt;/code&gt; to the private IP of the redis Fargate task for each ad hoc environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Load Balancing
&lt;/h3&gt;

&lt;p&gt;We now have our backend services (gunicorn, celery and celerybeat) running on Fargate spot instances, and these services can communicate with the redis service in our ad hoc environment's ECS cluster using service discovery that we configured with Cloud Map. We still need to think about a few things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how will we expose our API service to the public (or private) internet&lt;/li&gt;
&lt;li&gt;how will we expose our frontend application to the public (or private) internet&lt;/li&gt;
&lt;li&gt;how will we make sure that requests go the correct ECS services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Application load balancers (ALBs) are a great way to expose web app traffic to the internet. We could either have one application load balancer per ad hoc environment, or one application load balancer shared between all ad hoc environments. ALBs are somewhat slow to create and they also incur a significant monthly cost. They are also highly scalable, so using a shared ALB for all ad hoc environments would work.&lt;/p&gt;

&lt;p&gt;Individual ad hoc environments can then create target groups and listener rules for a shared ALB for each service that needs to serve requests from the internet (the backend and the frontend). In our case this is the backend API server and the frontend server that serves our static frontend site using NGINX.&lt;/p&gt;

&lt;p&gt;ECS services that need to be exposed to the internet can specify the target group, port and container to use for load balancing. A target group is created that defines the health check and other settings, and a load balancer listener rule is created on the shared load balancer that will forward traffic matching certain conditions to the target group for our service.&lt;/p&gt;

&lt;p&gt;For a given ad hoc environment, we need to specify that only traffic with certain paths should be sent to the backend service, and all other traffic should be sent to the frontend service. For example, we may only want to send traffic that starts with the path &lt;code&gt;/api&lt;/code&gt; or &lt;code&gt;/admin&lt;/code&gt; to the backend target group, and all other traffic should be sent to the frontend target group. We can do this by setting conditions on the listener rules that forward traffic do the frontend and backend target groups based on the hostname and path.&lt;/p&gt;

&lt;p&gt;We want our listener rule logic to forward &lt;code&gt;/api&lt;/code&gt;, &lt;code&gt;/admin&lt;/code&gt; and any other backend traffic to the backend target group, and forward all other traffic (&lt;code&gt;/*&lt;/code&gt;) to the frontend target group. In order to do this, we need the backend listener rule to have a higher priority than the frontend listener rule for each ad hoc environment. Since we are using the same load balancer for all ad hoc environments, the priority values for each listener rule need to be unique. If we don't set the priority explicitly, then the priority will be set automatically to the next available value in ascending order. In order to make sure that the backend listener rule has a higher priority than the frontend listener rule for each ad hoc environment, we need to tell Terraform that the frontend module &lt;code&gt;depends_on&lt;/code&gt; the backend module. This way the backend listener rule will have a higher priority (e.g. priority of 1) because it will be created first, and the frontend listener rule will have a lower priority (e.g. priority of 2).&lt;/p&gt;

&lt;h2&gt;
  
  
  More on shared resources vs per-environment resources
&lt;/h2&gt;

&lt;p&gt;Up until now we have discussed infrastructure design decisions at a high level, but we have not yet talked about how to organize our infrastructure as code. At a basic level, components of our ad hoc environment either fall into shared infrastructure or infrastructure that is specific to an individual ad hoc environment. Here's a list of the resources that are shared and the resources that are specific to each ad hoc environment.&lt;/p&gt;

&lt;p&gt;Shared resources include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPC&lt;/li&gt;
&lt;li&gt;IAM policies&lt;/li&gt;
&lt;li&gt;Security groups&lt;/li&gt;
&lt;li&gt;RDS instance&lt;/li&gt;
&lt;li&gt;Service Discovery namespace&lt;/li&gt;
&lt;li&gt;Application Load Balancer&lt;/li&gt;
&lt;li&gt;Bastion host&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ad hoc environment resources include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ECS Cluster&lt;/li&gt;
&lt;li&gt;ECS Tasks and Services (for backend and frontend applications)&lt;/li&gt;
&lt;li&gt;ECS Tasks for running management commands (such as migrate)&lt;/li&gt;
&lt;li&gt;CloudWatch logging groups for containers defined in ECS Tasks&lt;/li&gt;
&lt;li&gt;ALB Target groups&lt;/li&gt;
&lt;li&gt;ALB listener rules&lt;/li&gt;
&lt;li&gt;Route 53 record that points to the load balancer (e.g. &lt;code&gt;ad-hoc-env-name.example.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;S3 bucket for static and media assets&lt;/li&gt;
&lt;li&gt;Service Discovery Service for redis service in ECS cluster&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shared resources can be defined in one terraform configuration and deployed once. These resources will be long-lived as long as the application is under active development and the team requires on-demand provisioning of ad hoc environments.&lt;/p&gt;

&lt;p&gt;Ad hoc environment resources can be defined in another terraform configuration that references outputs from the shared resource configuration using &lt;code&gt;terraform_remote_state&lt;/code&gt;. Each ad hoc environment can be defined by a &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file that contains the name of the ad hoc environment (such as &lt;code&gt;brian&lt;/code&gt;, &lt;code&gt;brian2&lt;/code&gt;, &lt;code&gt;demo-feature-abc&lt;/code&gt;, etc.). This &lt;code&gt;&amp;lt;name&amp;gt;&lt;/code&gt; value will also be the name of the Terraform workspace and will be used to name and tag AWS resources associated with the corresponding ad hoc environment.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file will allow developers to use a simple, standard file interface for defining application specific values, such as the version of the backend and frontend. This brings developers into the concepts and practices of "infrastructure as code" and "configuration as code" and also helps the entire team keep track of how different environments are configured.&lt;/p&gt;

&lt;p&gt;Ad hoc environment &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; files are stored in a directory of a special git repository that also defines the ad hoc environment terraform configuration. Currently, the &lt;code&gt;tfvars&lt;/code&gt; files are stored &lt;a href="https://github.com/briancaffey/django-step-by-step/tree/main/terraform/live/ad-hoc/envs" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now let's look at the two terraform configurations used for defining &lt;strong&gt;shared resources&lt;/strong&gt; and &lt;strong&gt;ad hoc environment resources&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ad Hoc Environment Diagram
&lt;/h2&gt;

&lt;p&gt;Here's an overview of the resources used for the ad hoc environments. The &lt;strong&gt;letters represent shared resources&lt;/strong&gt; and the &lt;strong&gt;numbers represent per-environment resources&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F320yfb4i8vl22ptr6m2r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F320yfb4i8vl22ptr6m2r.png" alt="Ad hoc environment diagram" width="800" height="661"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Shared architecture
&lt;/h3&gt;

&lt;p&gt;A. VPC (created using the &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest" rel="noopener noreferrer"&gt;official AWS VPC Module&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;B. Public subnets for bastion host, NAT Gateways and Load Balancer&lt;/p&gt;

&lt;p&gt;C. Private subnets for application workloads and RDS&lt;/p&gt;

&lt;p&gt;D. Application Load Balancer that is shared between all ad hoc environments. A pre-provisioned wildcard ACM certificate is attached to the load balancer that is used to secure traffic for load-balanced ECS services&lt;/p&gt;

&lt;p&gt;E. Service discovery namespace that provides a namespace for application workloads to access the redis service running in ECS&lt;/p&gt;

&lt;p&gt;F. IAM roles needed for ECS tasks to access AWS services&lt;/p&gt;

&lt;p&gt;G. RDS instance using postgres engine that is shared between all ad hoc environments&lt;/p&gt;

&lt;p&gt;H. Bastion host used to access RDS from GitHub Actions (needed for creating per-environment databases)&lt;/p&gt;

&lt;p&gt;I. NAT Gateway used to give traffic in private subnets a route to the public internet&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment-specific architecture
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;ECS Cluster that groups all ECS tasks for a single ad hoc environment&lt;/li&gt;
&lt;li&gt;Listener rules and target groups that direct traffic from the load balancer to the ECS services for an ad hoc environment.&lt;/li&gt;
&lt;li&gt;Redis service running in ECS that provides caching and serves as a task broker for celery&lt;/li&gt;
&lt;li&gt;Route53 records that point to the load balancer&lt;/li&gt;
&lt;li&gt;Frontend service that serves the Vue.js application over NGINX&lt;/li&gt;
&lt;li&gt;API service that serves the backend with Gunicorn&lt;/li&gt;
&lt;li&gt;Celery worker that process jobs in the default queue&lt;/li&gt;
&lt;li&gt;Celery beat that schedules celery tasks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;collectstatic&lt;/code&gt; task&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;migrate&lt;/code&gt; task&lt;/li&gt;
&lt;li&gt;CloudWatch log groups are created for each ECS task in an ad hoc environment&lt;/li&gt;
&lt;li&gt;Each ad hoc environment gets a database in the shared RDS instance&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Shared resources terraform configuration
&lt;/h2&gt;

&lt;p&gt;Let's have a detailed look at the terraform configuration for shared resources that will support ad hoc environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPC
&lt;/h3&gt;

&lt;p&gt;We can use the &lt;a href="https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest" rel="noopener noreferrer"&gt;AWS VPC module&lt;/a&gt; for creating the shared VPC with Terraform. This module provides a high level interface that will provision lots of the components that are needed for a VPC following best practices, and it is less code for the DevOps team to manage compared to defining each component of a VPC (route tables, subnets, internet gateways, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud Map Service Discovery Namespace
&lt;/h3&gt;

&lt;p&gt;Cloud Map is used in order to allow services in our ECS cluster to communicate with each other. The only reason that Cloud Map is needed is so that the backend services (API, celery workers, beat) can communicate with Redis, which will be an important service for our application, providing caching and also serving as a broker for celery. If we were to use Django Channels for websockets, the Redis service would also function as the backend for Django Channels.&lt;/p&gt;

&lt;p&gt;We will only need to specify &lt;code&gt;service_registries&lt;/code&gt; on the redis service in our ECS cluster. What this will do is provide an address that our other services can use to communicate with redis. This address is created in the form of a Route 53 record, and it points to the private IP address of the redis service. If the private IP of the redis service is updated, the Route 53 record record for our redis service will be updated as well.&lt;/p&gt;

&lt;p&gt;In order for service discovery to work in the VPC that we created, we need to add the following options to the terraform AWS VPC module:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# DNS settings
enable_dns_hostnames = true
enable_dns_support   = true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h3&gt;
  
  
  Security Groups
&lt;/h3&gt;

&lt;p&gt;There are two important security groups that we will set up as part of the shared infrastructure layer to be used by each ad hoc environment: one security group for the load balancer, and one security group where all of our ECS services will run.&lt;/p&gt;

&lt;p&gt;The load balancer security group will allow all traffic on port &lt;code&gt;80&lt;/code&gt; and &lt;code&gt;443&lt;/code&gt; for &lt;code&gt;HTTP&lt;/code&gt; and &lt;code&gt;HTTPS&lt;/code&gt; traffic. The ECS security group will only allow inbound traffic from the application load balancer security group. It will also allow for traffic from port 6379 for redis traffic.&lt;/p&gt;
&lt;h3&gt;
  
  
  IAM Roles
&lt;/h3&gt;

&lt;p&gt;There are two important IAM roles that we will need for our ECS tasks. We need a task execution role that our ECS tasks will use to interact with other AWS services, such as S3, Secrets Manager, etc.&lt;/p&gt;
&lt;h3&gt;
  
  
  RDS Instance
&lt;/h3&gt;

&lt;p&gt;We will create one RDS instance in one of the private subnets in our VPC. This RDS instance will have one Postgres database per ad hoc environment. This RDS instance has a security group that allows all traffic from our ECS security group.&lt;/p&gt;
&lt;h3&gt;
  
  
  Load Balancer
&lt;/h3&gt;

&lt;p&gt;We will use one load balancer for all ad hoc environments. This load balancer will have a wildcard ACM certificate attached to it (&lt;code&gt;*.dev.example.com&lt;/code&gt;, for example). Each ad hoc environment will create a Route 53 record that will point to this load balancer's public DNS name. For example, &lt;code&gt;brian.dev.example.com&lt;/code&gt; will be the address of my ad hoc environment. Requests to this address will then be routed to either the frontend ECS service or the backend ECS service depending on request header values and request path values that will be set on the listener rules.&lt;/p&gt;

&lt;p&gt;By default, a load balancer supports up to 50 listener rules, so we can create plenty of ad hoc environments before we need to increase the default quota. There will be a discussion at the end of this article about AWS service quotas.&lt;/p&gt;
&lt;h3&gt;
  
  
  Bastion Host
&lt;/h3&gt;

&lt;p&gt;The bastion host will be created in one of the VPC's public subnets. This will primarily be used for connecting to RDS to create new databases for new ad hoc environments, or for manually manipulating data in an ad hoc environment for debugging.&lt;/p&gt;
&lt;h2&gt;
  
  
  Ad hoc environment resources
&lt;/h2&gt;

&lt;p&gt;Now that we have defined a shared set of infrastructure that our ad hoc environments will use, let's have a look at the resources that will be specific to ad hoc environments that will be added on top of the shared resources.&lt;/p&gt;
&lt;h3&gt;
  
  
  ECS Cluster
&lt;/h3&gt;

&lt;p&gt;The ECS Cluster is a simple grouping of ECS tasks and services.&lt;/p&gt;
&lt;h3&gt;
  
  
  ECS Tasks and Services
&lt;/h3&gt;

&lt;p&gt;Each environment will have a set of ECS tasks and services that will be used to run the application.&lt;/p&gt;

&lt;p&gt;There are four important ECS services in our application that are used to run "long-running" ECS tasks. Long-running tasks are tasks that start processes that run indefinitely, rather than running until completion. The long-running tasks in our application include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;backend web application (gunciron web server)&lt;/li&gt;
&lt;li&gt;backend celery worker&lt;/li&gt;
&lt;li&gt;backend celery beat&lt;/li&gt;
&lt;li&gt;frontend web site (nginx web server)&lt;/li&gt;
&lt;li&gt;redis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The infrastructure code also defines some tasks that are not long-running but rather short lived tasks that run until completion and do not start again. These tasks include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;collectstatic&lt;/li&gt;
&lt;li&gt;database migrations&lt;/li&gt;
&lt;li&gt;any other ad-hoc task that we want to run, usually wrapped in a Django management command&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How to setup an ad hoc environment
&lt;/h2&gt;

&lt;p&gt;Now that we have been over the resources that will be created to support our ad hoc environments, let's talk about how we can enable individuals on our team to create and update ad hoc environments.&lt;/p&gt;
&lt;h3&gt;
  
  
  Design decisions
&lt;/h3&gt;

&lt;p&gt;The devops team will decide on the interface that will be used for creating an ad hoc environment. Since we are using Terraform, this interface will be a Terraform configuration. The minimum amount of information that our ad hoc environment configuration needs is image tags for the frontend and backend images to use. Other configurations will be provided by default values set in &lt;code&gt;variables.tf&lt;/code&gt;, and these defaults can easily be overridden by passing values to &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt;. I'm choosing to use &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; as the way to pass configuration values to our ad hoc environments where &lt;code&gt;&amp;lt;name&amp;gt;&lt;/code&gt; is the name of the ad hoc environment being created. This will give us the following benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;all ad hoc environments will be visible to the entire team in git since each ad hoc environment will have a &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file associated with it&lt;/li&gt;
&lt;li&gt;adding additional customization to an ad hoc environment does not add additional complexity to our automation pipeline since all customization is added through a single file that will be referenced by &lt;code&gt;$WORKSPACE.tfvars&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The downsides of this approach are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creating ad hoc environments requires knowledge of git, so non-technical product team members might need help from the engineering team when setting up an ad hoc environment&lt;/li&gt;
&lt;li&gt;there is an additional "manual" step of creating a &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file that must be done before running a pipeline to create an ad hoc environment&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Provided that a &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file has been created and pushed to the repo, creating or updating an ad hoc environment will be as simple as running a pipeline in GitHub Actions that specifies the &lt;code&gt;&amp;lt;name&amp;gt;&lt;/code&gt; of our ad hoc environment. If no such &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file exists, our pipeline will fail.&lt;/p&gt;
&lt;h3&gt;
  
  
  GitHub Action
&lt;/h3&gt;

&lt;p&gt;Creating ad hoc environments will involve manually triggering a GitHub Action that runs on &lt;code&gt;workflow_dispatch&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;on:
  workflow_dispatch:
    inputs:
      workspace:
        description: 'Name of terraform workspace to use'
        required: true
        default: 'dev'
        type: string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We only have to enter the name of the ad hoc environment we want to create or update. The ad hoc environment name is used as the Terraform workspace name. This name is also the name of the &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file that must be created per environment.&lt;/p&gt;

&lt;p&gt;This workflow will do &lt;code&gt;terraform init&lt;/code&gt;, &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt; using the &lt;code&gt;&amp;lt;name&amp;gt;.tfvars&lt;/code&gt; file. When everything has been created, we will use the AWS CLI to prepare the environment so that it can be used. We will use the &lt;code&gt;aws ecs run-task&lt;/code&gt; command to run database migrations needed so that the application code can make database queries.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to update code in an existing ad hoc environment
&lt;/h2&gt;

&lt;p&gt;Assuming that we have deployed an ad hoc environment called &lt;code&gt;brian&lt;/code&gt; with version &lt;code&gt;v1.0.0&lt;/code&gt; of the backend application and &lt;code&gt;v2.0.0&lt;/code&gt; of the frontend application, let's think about the process of updating the application to &lt;code&gt;v1.1.0&lt;/code&gt; of the backend and &lt;code&gt;v2.1.0&lt;/code&gt; of the frontend.&lt;/p&gt;

&lt;p&gt;The simplest approach to updating the application would be edit the &lt;code&gt;brian.tfvars&lt;/code&gt; file with the new versions:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# brian.tfvars
be_image_tag = "v1.1.0"
fe_image_tag = "v2.1.0"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If we run the same pipeline that we initially used to deploy ad hoc environment (with &lt;code&gt;terraform init&lt;/code&gt;, &lt;code&gt;terraform plan&lt;/code&gt; and &lt;code&gt;terraform apply&lt;/code&gt;) against the updated &lt;code&gt;brian.tfvars&lt;/code&gt; file, this will result in a rolling update of the frontend and backend services (&lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html" rel="noopener noreferrer"&gt;more on rolling updates here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;If there are database migrations included in the new version of the code that is going out, we need to run database migrations after the &lt;code&gt;terraform apply&lt;/code&gt; completes. We use a top level output from the ad hov environment terraform configuration that is a &lt;code&gt;run-task&lt;/code&gt; command with all appropriate arguments that will run database migrations when called from GitHub Actions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Order of Operations
&lt;/h3&gt;

&lt;p&gt;For ad hoc environments, it is probably fine to update the services and then run the database migrations. Ad hoc environments may only have a single "user" -- the developer, so &lt;strong&gt;we don't need to worry about any errors that may occur if requests are made against the new version of code before database migrations have been applied&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Let's consider a simple example to illustrate what can go wrong here. If we add a &lt;code&gt;total_views&lt;/code&gt; to our blog post model to track the total number of page views a post has, we would add a field to the model, generate migration file with &lt;code&gt;makemigrations&lt;/code&gt;, and then update our views and model serializers to make use of this new field. In the time between updating our service and running the database migrations, any requests to endpoints that access the new database field will fail since the table does not yet exist.&lt;/p&gt;

&lt;p&gt;If we first run database migrations &lt;strong&gt;and then&lt;/strong&gt; update application code (ECS services), then we can avoid errors about fields not existing. In our production application, we want to aim for fewer errors, so we should be using this "order of operations": first run new database migrations and then update application code.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Action for application updates
&lt;/h3&gt;

&lt;p&gt;We need a GitHub Action that can do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch the current container definition JSON files for each backend tasks (&lt;code&gt;aws ecs describe-task-definition&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;write new container definitions JSON with the new backend image tag (using &lt;code&gt;jq&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;register new task definitions with the new container definition JSON files for each task (&lt;code&gt;aws ecs register-task-definition&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;call run-task with the newly updated migration ECS task (&lt;code&gt;aws ecs run-task&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;wait for the task to exit and display the logs (&lt;code&gt;aws ecs wait tasks-stopped&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;update the backend services (gunicorn, celery, celery beat) (&lt;code&gt;aws ecs update-service&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;wait for the new backend services to be stable (&lt;code&gt;aws ecs wait services-stable&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a visual representation of the backend update process:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxn85ri3brjxa7x6fsey.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Foxn85ri3brjxa7x6fsey.png" alt="Backend application update workflow" width="800" height="951"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In order have the correct arguments for all of the AWS CLI calls used in the above workflow, we can use the AWS CLI to fetch resource names by tag.&lt;/p&gt;

&lt;p&gt;Here is what I'm using for the script. There lots of comments, so please refer to those comments for an explanation of what the script is doing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# This script will be called to update an ad hoc environment backend&lt;/span&gt;
&lt;span class="c"&gt;# with a new image tag. It will first run pre-update tasks (such as migrations)&lt;/span&gt;
&lt;span class="c"&gt;# and then do a rolling update of the backend services.&lt;/span&gt;

&lt;span class="c"&gt;# It is called from the ad_hock_backend_update.yml GitHub Actions file&lt;/span&gt;

&lt;span class="c"&gt;# Required environment variables that need to be exported before running this script:&lt;/span&gt;

&lt;span class="c"&gt;# WORKSPACE - ad hoc environment workspace&lt;/span&gt;
&lt;span class="c"&gt;# SHARED_RESOURCES_WORKSPACE - shared resources workspace&lt;/span&gt;
&lt;span class="c"&gt;# BACKEND_IMAGE_TAG - backend image tag to update services to (e.g. v1.2.3)&lt;/span&gt;
&lt;span class="c"&gt;# AWS_ACCOUNT_ID - AWS account ID is used for the ECR repository URL&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Updating backend services..."&lt;/span&gt;

&lt;span class="c"&gt;# first define a variable containing the new image URI&lt;/span&gt;
&lt;span class="nv"&gt;NEW_BACKEND_IMAGE_URI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AWS_ACCOUNT_ID&lt;/span&gt;&lt;span class="s2"&gt;.dkr.ecr.us-east-1.amazonaws.com/backend:&lt;/span&gt;&lt;span class="nv"&gt;$BACKEND_IMAGE_TAG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;


&lt;span class="c"&gt;# register new task definitions&lt;/span&gt;
&lt;span class="c"&gt;# https://docs.aws.amazon.com/cli/latest/reference/ecs/describe-task-definition.html#description&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;TASK &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"migrate"&lt;/span&gt; &lt;span class="s2"&gt;"gunicorn"&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="s2"&gt;"beat"&lt;/span&gt;
&lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Updating &lt;/span&gt;&lt;span class="nv"&gt;$TASK&lt;/span&gt;&lt;span class="s2"&gt; task definition..."&lt;/span&gt;

  &lt;span class="c"&gt;# in Terraform we name our tasks based on the ad hoc environment name&lt;/span&gt;
  &lt;span class="c"&gt;# (also the Terraform workspace name) and the name of the task&lt;/span&gt;
  &lt;span class="c"&gt;# (e.g. migrate, gunicorn, default, beat)&lt;/span&gt;
  &lt;span class="nv"&gt;TASK_FAMILY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;-&lt;span class="nv"&gt;$TASK&lt;/span&gt;

  &lt;span class="c"&gt;# save the task definition JSON to a variable&lt;/span&gt;
  &lt;span class="nv"&gt;TASK_DESCRIPTION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecs describe-task-definition &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task-definition&lt;/span&gt; &lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;# save container definitions to a file for each task&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DESCRIPTION&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.containerDefinitions &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;.json

  &lt;span class="c"&gt;# write new container definition JSON with updated image&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Writing new &lt;/span&gt;&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="s2"&gt; container definitions JSON..."&lt;/span&gt;

  &lt;span class="c"&gt;# replace old image URI with new image URI in a new container definitions JSON&lt;/span&gt;
  &lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;.json &lt;span class="se"&gt;\&lt;/span&gt;
    | jq &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--arg&lt;/span&gt; IMAGE &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$NEW_BACKEND_IMAGE_URI&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'.[0].image |= $IMAGE'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /tmp/&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="nt"&gt;-new&lt;/span&gt;.json

  &lt;span class="c"&gt;# Get the existing configuration for the task definition (memory, cpu, etc.)&lt;/span&gt;
  &lt;span class="c"&gt;# from the variable that we saved the task definition JSON to earlier&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Getting existing configuration for &lt;/span&gt;&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="s2"&gt;..."&lt;/span&gt;

  &lt;span class="nv"&gt;MEMORY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DESCRIPTION&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.memory &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;CPU&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DESCRIPTION&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.cpu &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;ECS_EXECUTION_ROLE_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DESCRIPTION&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.executionRoleArn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="nv"&gt;ECS_TASK_ROLE_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DESCRIPTION&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.taskRoleArn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;# check the content of the new container definition JSON&lt;/span&gt;
  &lt;span class="nb"&gt;cat&lt;/span&gt; /tmp/&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="nt"&gt;-new&lt;/span&gt;.json

  &lt;span class="c"&gt;# register new task definition using the new container definitions&lt;/span&gt;
  &lt;span class="c"&gt;# and the values that we read off of the existing task definitions&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Registering new &lt;/span&gt;&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="s2"&gt; task definition..."&lt;/span&gt;

  aws ecs register-task-definition &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--family&lt;/span&gt; &lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--container-definitions&lt;/span&gt; file:///tmp/&lt;span class="nv"&gt;$TASK_FAMILY&lt;/span&gt;&lt;span class="nt"&gt;-new&lt;/span&gt;.json &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--memory&lt;/span&gt; &lt;span class="nv"&gt;$MEMORY&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cpu&lt;/span&gt; &lt;span class="nv"&gt;$CPU&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network-mode&lt;/span&gt; awsvpc &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--execution-role-arn&lt;/span&gt; &lt;span class="nv"&gt;$ECS_EXECUTION_ROLE_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task-role-arn&lt;/span&gt; &lt;span class="nv"&gt;$ECS_TASK_ROLE_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--requires-compatibilities&lt;/span&gt; &lt;span class="s2"&gt;"FARGATE"&lt;/span&gt;

&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Now we need to run migrate, collectstatic and any other commands that need to be run&lt;/span&gt;
&lt;span class="c"&gt;# before doing a rolling update of the backend services&lt;/span&gt;

&lt;span class="c"&gt;# We will use the new task definitions we just created to run these commands&lt;/span&gt;

&lt;span class="c"&gt;# get the ARN of the most recent revision of the migrate task definition&lt;/span&gt;
&lt;span class="nv"&gt;TASK_DEFINITION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws ecs describe-task-definition &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task-definition&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-migrate&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    .taskDefinition.taskDefinitionArn &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# get private subnets as space separated string from shared resources VPC&lt;/span&gt;
&lt;span class="nv"&gt;SUBNETS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws ec2 describe-subnets &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:env,Values=&lt;/span&gt;&lt;span class="nv"&gt;$SHARED_RESOURCES_WORKSPACE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=*private*"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Subnets[*].SubnetId'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; text &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# replace spaces with commas using tr&lt;/span&gt;
&lt;span class="nv"&gt;SUBNET_IDS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="nv"&gt;$SUBNETS&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# https://github.com/aws/aws-cli/issues/5348&lt;/span&gt;
&lt;span class="c"&gt;# get ecs_sg_id - just a single value&lt;/span&gt;
&lt;span class="nv"&gt;ECS_SG_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws ec2 describe-security-groups &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--filters&lt;/span&gt; &lt;span class="s2"&gt;"Name=tag:Name,Values=&lt;/span&gt;&lt;span class="nv"&gt;$SHARED_RESOURCES_WORKSPACE&lt;/span&gt;&lt;span class="s2"&gt;-ecs-sg"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'SecurityGroups[*].GroupId'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; text &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Running database migrations..."&lt;/span&gt;

&lt;span class="c"&gt;# timestamp used for log retrieval (milliseconds after Jan 1, 1970 00:00:00 UTC)&lt;/span&gt;
&lt;span class="nv"&gt;START_TIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s000&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# run the migration task and capture the taskArn into a variable called TASK_ID&lt;/span&gt;
&lt;span class="nv"&gt;TASK_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  aws ecs run-task &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-cluster&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task-definition&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DEFINITION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--network-configuration&lt;/span&gt; &lt;span class="s2"&gt;"awsvpcConfiguration={subnets=[&lt;/span&gt;&lt;span class="nv"&gt;$SUBNET_IDS&lt;/span&gt;&lt;span class="s2"&gt;],securityGroups=[&lt;/span&gt;&lt;span class="nv"&gt;$ECS_SG_ID&lt;/span&gt;&lt;span class="s2"&gt;],assignPublicIp=ENABLED}"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.tasks[0].taskArn'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Task ID is &lt;/span&gt;&lt;span class="nv"&gt;$TASK_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# wait for the migrate task to exit&lt;/span&gt;
&lt;span class="c"&gt;# https://docs.aws.amazon.com/cli/latest/reference/ecs/wait/tasks-stopped.html#description&lt;/span&gt;
&lt;span class="c"&gt;# &amp;gt; It will poll every 6 seconds until a successful state has been reached.&lt;/span&gt;
&lt;span class="c"&gt;# &amp;gt; This will exit with a return code of 255 after 100 failed checks.&lt;/span&gt;
aws ecs &lt;span class="nb"&gt;wait &lt;/span&gt;tasks-stopped &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--tasks&lt;/span&gt; &lt;span class="nv"&gt;$TASK_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-cluster&lt;/span&gt;

&lt;span class="c"&gt;# timestamp used for log retrieval (milliseconds after Jan 1, 1970 00:00:00 UTC)&lt;/span&gt;
&lt;span class="nv"&gt;END_TIME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s000&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# print the CloudWatch log events to STDOUT&lt;/span&gt;
aws logs get-log-events &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--log-group-name&lt;/span&gt; &lt;span class="s2"&gt;"/ecs/&lt;/span&gt;&lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="s2"&gt;/migrate"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--log-stream-name&lt;/span&gt; &lt;span class="s2"&gt;"migrate/migrate/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;TASK_ID&lt;/span&gt;&lt;span class="p"&gt;##*/&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start-time&lt;/span&gt; &lt;span class="nv"&gt;$START_TIME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end-time&lt;/span&gt; &lt;span class="nv"&gt;$END_TIME&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.events[].message'&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Migrations complete. Starting rolling update for backend services..."&lt;/span&gt;

&lt;span class="c"&gt;# update backend services&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;TASK &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"gunicorn"&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="s2"&gt;"beat"&lt;/span&gt;
&lt;span class="k"&gt;do&lt;/span&gt;

  &lt;span class="c"&gt;# get taskDefinitionArn for each service to be used in update-service command&lt;/span&gt;
  &lt;span class="c"&gt;# this will get the most recent revision of each task (the one that was just created)&lt;/span&gt;
  &lt;span class="c"&gt;# https://docs.aws.amazon.com/cli/latest/reference/ecs/describe-task-definition.html#description&lt;/span&gt;
  &lt;span class="nv"&gt;TASK_DEFINITION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    aws ecs describe-task-definition &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="nt"&gt;--task-definition&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;-&lt;span class="nv"&gt;$TASK&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      .taskDefinition.taskDefinitionArn &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="c"&gt;# update each service with new task definintion&lt;/span&gt;
  aws ecs update-service &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-cluster&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--service&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;-&lt;span class="nv"&gt;$TASK&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task-definition&lt;/span&gt; &lt;span class="nv"&gt;$TASK_DEFINITION&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--no-cli-pager&lt;/span&gt;

&lt;span class="k"&gt;done

&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Services updated. Waiting for services to become stable..."&lt;/span&gt;

&lt;span class="c"&gt;# wait for all service to be stable (runningCount == desiredCount for each service)&lt;/span&gt;
aws ecs &lt;span class="nb"&gt;wait &lt;/span&gt;services-stable &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-cluster&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--services&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-gunicorn&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-default&lt;/span&gt; &lt;span class="nv"&gt;$WORKSPACE&lt;/span&gt;&lt;span class="nt"&gt;-beat&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Services are now stable. Backend services are now up to date with &lt;/span&gt;&lt;span class="nv"&gt;$BACKEND_IMAGE_TAG&lt;/span&gt;&lt;span class="s2"&gt;."&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Backend update is now complete!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this GitHub Actions workflow, a developer can now easily change the version of backend code that is running in their ad hoc environments without needing to involve Terraform. Using the &lt;code&gt;ad_hoc_backend_update.yml&lt;/code&gt; GitHub Actions workflow, a developer only needs to enter the name of the workspace and the version of the backend code they want to use. The workflow will then run the &lt;code&gt;migrate&lt;/code&gt; task and update the backend services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using &lt;code&gt;ignore_changes&lt;/code&gt; in the definitions
&lt;/h3&gt;

&lt;p&gt;There is one more important point to make about Terraform before we conclude this discussion on updating backend code for existing ad hoc environments. Consider the scenario where a developer has launched an ad hoc environment with backend version &lt;code&gt;v1.0.0&lt;/code&gt;. They make a small change to the backend code and push version &lt;code&gt;v1.0.1&lt;/code&gt;. Next, the developer remembers that a backend environment variable needs to be changed. This can be done by updating their &lt;code&gt;*.tfvars&lt;/code&gt; file. If they now re-run the ad hoc environment update pipeline &lt;strong&gt;without also updating the backend version in their &lt;code&gt;*.tfvars&lt;/code&gt; file&lt;/strong&gt;, then their code will be reverted from &lt;code&gt;v1.0.1&lt;/code&gt; to &lt;code&gt;v1.0.0&lt;/code&gt;. We would need to coordinate version changes between updating the backend with the pipelines that use Terraform commands and the pipelines that use the AWS CLI commands.&lt;/p&gt;

&lt;p&gt;There is a setting on the &lt;code&gt;aws_ecs_service&lt;/code&gt; resource in Terraform we can can use to prevent this from happening. This setting is called &lt;a href="https://www.terraform.io/language/meta-arguments/lifecycle" rel="noopener noreferrer"&gt;&lt;code&gt;ignore_changes&lt;/code&gt;&lt;/a&gt; and is defined under the resource's &lt;code&gt;lifecycle&lt;/code&gt; configuration block. With this setting, when we update the &lt;code&gt;*.tfvars&lt;/code&gt; file with our new environment variable value, we will create another recent task definition with the same &lt;code&gt;v1.0.0&lt;/code&gt; image, but the ECS service will not update in response to this change (that's what the &lt;code&gt;ignore_changes&lt;/code&gt; is for). Once we make the &lt;code&gt;*.tfvars&lt;/code&gt; file update and redeploy using the Terraform pipeline, nothing on our ad hoc changes, but we did get a new task definitions defined in our AWS account for each backend service. When we go to make the backend update with the pipeline that uses AWS CLI commands, the most recent task revision is used to create the new task definition, so it will include the environment variable change that we added earlier.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frontend updates
&lt;/h3&gt;

&lt;p&gt;The process described above is needed for updating the backend application. Updating the frontend application involves a similar process to the backend update. The main difference is that no task (such as the &lt;code&gt;migrate&lt;/code&gt; command on the backend) needs to run before the service is updated. Here's an overview of the frontend update process:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;fetch the container definition JSON for the frontend tasks (&lt;code&gt;aws ecs describe-task-definition&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;write new container definition JSON with the new frontend image tag (using &lt;code&gt;jq&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;register new task definitions with the new container definition JSON file for the frontend task (&lt;code&gt;aws ecs register-task-definition&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;update the frontend service (&lt;code&gt;aws ecs update-service&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;wait for the new backend services to be stable (&lt;code&gt;aws ecs wait services-stable&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up everything from a new AWS account and GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Here's a quick overview of initial setup steps that are needed in order to use the automation defined in the GitHub Actions for ad hoc environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure AWS credentials locally
&lt;/h3&gt;

&lt;p&gt;There is one Terraform command that we will run on our local machine to setup a remote backend for storing our Terraform state. In order to run this, we need to configure AWS credentials locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Run the &lt;code&gt;make tf-bootstrap&lt;/code&gt; command
&lt;/h3&gt;

&lt;p&gt;This command will setup an S3 bucket and DynamoDB table for storing Terraform state. Running this command will also require that a &lt;code&gt;bootstrap.tfvars&lt;/code&gt; file has been created from the template. This will define the AWS region and name to be used for creating resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Build and push a backend and frontend image
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;tf-bootstrap&lt;/code&gt; command also creates ECR repositories for the backend and frontend images. We can use the &lt;code&gt;ecr_backend.yml&lt;/code&gt; and &lt;code&gt;ecr_frontend.yml&lt;/code&gt; GitHub Actions workflows to build and push the backend and frontend images to the ECR repositories. These pipelines accept a single parameter which is a git tag that must exist in the repo. This git tag will then be used as the image tag for the backend and frontend images.&lt;/p&gt;

&lt;h3&gt;
  
  
  Purchase a domain name in Route 53
&lt;/h3&gt;

&lt;p&gt;I use Route 53 to manage DNS records, and have purchased a domain name that I use for testing and debugging in this and other projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a wildcard ACM certificate
&lt;/h3&gt;

&lt;p&gt;I chose to create this certificate outside of Terraform and import it via ARN. We need a wildcard certificate so that multiple ad hoc environments can be hosted on subdomains of the same domain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a new EC2 key pair
&lt;/h3&gt;

&lt;p&gt;The key pair should be created manually and it needs to be added to GitHub repository secrets so that it can be used in the ad hoc environment pipelines.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add secrets to GitHub
&lt;/h3&gt;

&lt;p&gt;The following secrets are needed for GitHub Actions to run. Add these as repository secrets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ACM_CERTIFICATE_ARN&lt;/code&gt; - ARN of the wildcard ACM certificate&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; - AWS access key ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_ACCOUNT_ID&lt;/code&gt; - AWS account ID&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_DEFAULT_REGION&lt;/code&gt; - AWS default region (I use &lt;code&gt;us-east-1&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; - AWS secret access key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DOMAIN_NAME&lt;/code&gt; - domain name for the ad hoc environment (e.g. example.com)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;KEY_NAME&lt;/code&gt; - name of the EC2 key pair&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SSH_PRIVATE_KEY&lt;/code&gt; - private key for the EC2 key pair&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TF_BACKEND_BUCKET&lt;/code&gt; - name of the S3 bucket used for storing Terraform state&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TF_BACKEND_DYNAMODB_TABLE&lt;/code&gt; - name of the DynamoDB table used for locking the Terraform state file&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TF_BACKEND_REGION&lt;/code&gt; - AWS region for the S3 bucket and DynamoDB table&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These secrets are referenced in the GitHub Actions workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create shared resources
&lt;/h3&gt;

&lt;p&gt;Now that we have GitHub secrets configured, we can run the &lt;code&gt;shared_resources_create_update.yml&lt;/code&gt; GitHub Actions workflow. This will create  shared resources environment in which we can build our ad hoc environments. This workflow requires a name (e.g. &lt;code&gt;dev&lt;/code&gt;). This require that we create a &lt;code&gt;dev.tfvars&lt;/code&gt; file in &lt;code&gt;terraform/live/shared-resources&lt;/code&gt; directory. This usually takes between 5 and 7 minutes to complete since it needs to create an RDS instance which takes a few minutes to provision.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create an ad hoc environment
&lt;/h3&gt;

&lt;p&gt;We can now create an ad hoc environment. This requires the name of the shared resources environment (e.g. &lt;code&gt;dev&lt;/code&gt;) and the name of the ad hoc environment (e.g. &lt;code&gt;brian-test&lt;/code&gt;). The only thing we need to do before creating the &lt;code&gt;brian-test&lt;/code&gt; ad hoc environment is to create the &lt;code&gt;brian-test.tfvars&lt;/code&gt; file in the &lt;code&gt;terraform/live/ad-hoc/envs&lt;/code&gt; directory. This will define the versions of the application and any other environment configuration that is needed. This pipeline usually takes about 3 minutes to finish.&lt;/p&gt;

&lt;h3&gt;
  
  
  Update the backend version in an ad hoc environment
&lt;/h3&gt;

&lt;p&gt;Now that the backend is up and running and usable, we can update the backend version used in our ad hoc environment. This can be done by running the &lt;code&gt;ad_hoc_backend_update.yml&lt;/code&gt; GitHub Actions workflow. To run this workflow you must specify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the shared resource workspace (e.g. &lt;code&gt;dev&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the ad hoc environment name (e.g. &lt;code&gt;brian-test&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the new version of the backend to be used (e.g. &lt;code&gt;v1.0.2&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The backend version should already have been built and pushed to the ECR repository using the &lt;code&gt;ecr_backend.yml&lt;/code&gt; GitHub Actions workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use ECS Exec to access a Django shell in a running container
&lt;/h3&gt;

&lt;p&gt;Instead of SSHing into the container, we can use the &lt;code&gt;ecs-exec&lt;/code&gt; command to access a shell in the container. This is useful for debugging and testing. One of the outputs of the ad hoc environment terraform configuration contains the script needed to run the &lt;code&gt;ecs-exec&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TASK_ARN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ecs list-tasks &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; alpha-cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--service-name&lt;/span&gt;  alpha-gunicorn | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.taskArns | .[0]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="si"&gt;)&lt;/span&gt;
aws ecs execute-command &lt;span class="nt"&gt;--cluster&lt;/span&gt; alpha-cluster &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--task&lt;/span&gt; &lt;span class="nv"&gt;$TASK_ARN&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--container&lt;/span&gt; gunicorn &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--interactive&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--command&lt;/span&gt; &lt;span class="s2"&gt;"/bin/bash"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will then have a shell in the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-073d1947fa71c058c
root@ip-10-0-2-167:/code# python manage.py shell
Python 3.9.9 (main, Dec 21 2021, 10:03:34)
[GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Destroy an ad hoc environment
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;ad_hoc_env_destroy.yml&lt;/code&gt; GitHub Actions workflow to destroy an ad hoc environment. To run this workflow you need to specify the shared resource workspace (e.g. &lt;code&gt;dev&lt;/code&gt;) and the ad hoc environment name (e.g. &lt;code&gt;brian-test&lt;/code&gt;).&lt;/p&gt;

&lt;h3&gt;
  
  
  Destroy the shared resources environment
&lt;/h3&gt;

&lt;p&gt;Use the &lt;code&gt;shared_resources_destroy.yml&lt;/code&gt; GitHub Actions workflow to destroy the shared resources environment. To run this workflow you need to specify the shared resource workspace (e.g. &lt;code&gt;dev&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;This defines the full lifecycle of creating and destroying ad hoc environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future improvements, open questions and next steps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Enhanced Security
&lt;/h3&gt;

&lt;p&gt;The low hanging fruit here is to use least privilege (for roles used in automation) and to define all roles with IaC. Currently I am using Admin roles for the credentials I store in GitHub which is a shortcut to using IaC and is not a best practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keeping track of ad hoc environments
&lt;/h3&gt;

&lt;p&gt;We need to think about how we can keep track of our active ad hoc environments. Active environments will incur additional AWS costs, and we do not want developers or the product team to create lots of environments and then leave them running without actively using them.&lt;/p&gt;

&lt;p&gt;We may decide to have some long-lived ad hoc environments, but those would be managed primarily by the DevOps team and respective owners (e.g. QA, product team, etc.).&lt;/p&gt;

&lt;p&gt;One way to check the active ad hoc environments would be to use the AWS CLI. We could list the ECS clusters in our development account, and this would show the number of ad hoc environments running. We could go farther and list the ad hoc environments by when they were last updated. We could then request developers or team members to remove ad hoc environments that are not in use.&lt;/p&gt;

&lt;p&gt;Or we could have a policy that all ad-hoc environments are deleted automatically at the end of each week.&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform Tooling
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Testing Terraform Code&lt;/li&gt;
&lt;li&gt;Testing GitHub Actions&lt;/li&gt;
&lt;li&gt;Using advanced Terraform frameworks like Terragrunt&lt;/li&gt;
&lt;li&gt;Using Terraform Cloud for more advanced Terraform features&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  More secure way of defining RDS username and password
&lt;/h3&gt;

&lt;p&gt;Currently the postgres database does not have a secure password. It is both hardcoded in the module as a default value and it will also be saved in plaintext in the Terraform state file.&lt;/p&gt;

&lt;h3&gt;
  
  
  Backend application update script
&lt;/h3&gt;

&lt;p&gt;The script used for updating the backend application could be improved or broken up into multiple scripts to better handle errors and failures that happen during the pipeline. The script runs several different commands and could potentially fail at any step, so it would be nice to improve the error messages so that both developers and DevOps teams can more quickly diagnose pipeline failures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Limiting traffic to ad hoc environments to a VPN
&lt;/h3&gt;

&lt;p&gt;Another good next step would be to show how we can limit traffic to ad hoc environments to a VPN.&lt;/p&gt;

&lt;h3&gt;
  
  
  Figure out how many ad hoc environments we can create with the default quotas
&lt;/h3&gt;

&lt;p&gt;AWS accounts limit the number of resources you can create, but for most of this quota limits you can request an increase per account. I need to figure out how many ad hoc environments I can create with the default quotas.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Repo Organization
&lt;/h3&gt;

&lt;p&gt;One minor improvement would be to move the &lt;code&gt;terraform&lt;/code&gt; directory out of the &lt;code&gt;django-step-by-step&lt;/code&gt; monorepo into a dedicated repo. We may also want to move GitHub Actions for creating, updating and destroying environments to this new repo. For early stage development, using a single repository that stores both application code and Terraform configuration works, but it would be better to keep these separate at the repository level as the project grows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Multiple AWS Accounts
&lt;/h3&gt;

&lt;p&gt;Everything shown here uses a single AWS account: ECR images, Terraform remote state storage, all shared resource environments and all ad hoc environments. Using one account keeps things simple for a demonstration of this workflow, but in practice it would be beneficial to use multiple AWS accounts for different purposes. This would also involve more carefully planned IAM roles for cross-account resource access.&lt;/p&gt;

&lt;h3&gt;
  
  
  Modules for stable environments to be used for long-lived pre-production and production environments
&lt;/h3&gt;

&lt;p&gt;This article looked at how to make tradeoffs between costs, speed of deployment and production parity in ad hoc environments. I'm interested in building a new set of modules that can be used to set up environments that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;are more stable and more long-lived&lt;/li&gt;
&lt;li&gt;have less resource sharing (dedicated RDS and ElastiCache resources)&lt;/li&gt;
&lt;li&gt;implement autoscaling for load-testing (or maybe implement autoscaling for ad hoc environments)&lt;/li&gt;
&lt;li&gt;can be used to perform load testing&lt;/li&gt;
&lt;li&gt;have enhanced observability tooling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These environments might be used as part of a QA process that does a final sign-off on a new set of features scheduled for deployment to production environments, for example.&lt;/p&gt;

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

&lt;p&gt;This wraps up the tour of my ad hoc environment infrastructure automation. Thank you for having a read through my article. If you have a similar (or different) approach to building ad hoc environments, I would love to hear about it.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>terraform</category>
      <category>github</category>
      <category>django</category>
    </item>
    <item>
      <title>How and why I added AdSense and an AdBlock detector to my personal website</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Sun, 07 Nov 2021 19:57:31 +0000</pubDate>
      <link>https://dev.to/briancaffey/how-and-why-i-added-adsense-and-an-adblock-detector-to-my-personal-website-5ag3</link>
      <guid>https://dev.to/briancaffey/how-and-why-i-added-adsense-and-an-adblock-detector-to-my-personal-website-5ag3</guid>
      <description>&lt;p&gt;If you are reading this article on &lt;a href="https://briancaffey.github.io/2021/10/31/how-and-why-i-added-adsense-and-adblock-detector-to-my-website" rel="noopener noreferrer"&gt;briancaffey.github.io/2021/10/31/how-and-why-i-added-adsense-and-adblock-detector-to-my-website&lt;/a&gt;, then you will be prompted to pause your ad blocker if you are using one. If you are &lt;em&gt;not&lt;/em&gt; using an ad blocker, I recommend that you consider installing one. Reading this article on a browser with AdBlock enabled will allow you to see how I detect AdBlock and ask people to pause it when they are on my site.&lt;/p&gt;

&lt;p&gt;This article is a deep dive on how I added ads to my site with Google AdSense and how I request that visitors to my site pause AdBlock so that I can make more money from Google AdSense.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I added AdSense to my site
&lt;/h2&gt;

&lt;p&gt;I have been enjoying using my GitHub Pages website to learn more about static sites, JAMStack and Nuxt.js, an awesome Vue.js framework with support for building statically generated sites. I have been able to learn and implement several different features which I have written about on my blog. Some examples include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Adding a Drift chat window so users can message me directly&lt;/li&gt;
&lt;li&gt;Implementing a contact form with formsubmit.io&lt;/li&gt;
&lt;li&gt;Using Vue.js components in Markdown files to add interactive elements to my articles (such as graphs)&lt;/li&gt;
&lt;li&gt;Adding a custom MailChimp newsletter sign-up form that is included in the footer of each page of my blog&lt;/li&gt;
&lt;li&gt;Adding an RSS feed for my blog&lt;/li&gt;
&lt;li&gt;Adding a site index and submitting it to Google&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I have also been learning the suite of Google tools for monitoring and measuring traffic to my site, including Google Analytics and Google Search Console. Google Search Console is helpful for understanding the search terms that people are using when searching Google that result in organic traffic to my site.&lt;/p&gt;

&lt;p&gt;At one point I found out that another website was using the same Google Tracking code that I had previously hard-coded into an old version of my website, and my Google Analytics started measuring traffic to URLs that I didn't recognize as belonging to my site. I was able to fix this by adding a Hostname filter rule in Google Analytics.&lt;/p&gt;

&lt;p&gt;One area that I have not had any experience with until recently is Google AdSense. Google AdSense allows you to place ads on your website. Here's an overview of what I did to get started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add a site in Google AdSense&lt;/li&gt;
&lt;li&gt;Submit my site for approval (this takes a few days)&lt;/li&gt;
&lt;li&gt;Install and configure the Google AdSense plugin for NuxtJS&lt;/li&gt;
&lt;li&gt;Add the &lt;code&gt;ads.txt&lt;/code&gt; file generated by Google AdSense to my site&lt;/li&gt;
&lt;li&gt;Confirm my address by entering a code that was mailed to me&lt;/li&gt;
&lt;li&gt;Connect a bank account to my Google AdSense account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the address confirmation code that I received from Google:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fih85i18camj6ca2h0lj9.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fih85i18camj6ca2h0lj9.jpeg" alt="Address confirmation" width="800" height="1066"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the config code for AdSense from &lt;code&gt;nuxt.config.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;  &lt;span class="cm"&gt;/*
   ** Nuxt.js modules
   */&lt;/span&gt;
  &lt;span class="nx"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="c1"&gt;// Doc: https://axios.nuxtjs.org/usage&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nuxtjs/axios&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Doc: https://github.com/nuxt/content&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nuxt/content&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// Doc: https://www.npmjs.com/package/@nuxtjs/sitemap&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nuxtjs/sitemap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nuxtjs/feed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nuxt-i18n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@nuxtjs/google-adsense&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;     &lt;span class="o"&gt;&amp;lt;--&lt;/span&gt; &lt;span class="nx"&gt;AdSense&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ca-pub-4924597640144289&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}]&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process was pretty simple. Google now automatically places ads on my site in a few different formats:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ads displayed on the top and bottom of the page&lt;/li&gt;
&lt;li&gt;popup ads displayed between route navigation&lt;/li&gt;
&lt;li&gt;ads automatically inserted into the body of the page between paragraphs in my articles&lt;/li&gt;
&lt;li&gt;ads that I place on articles explicitly using the &lt;code&gt;&amp;lt;adsbygoogle /&amp;gt;&lt;/code&gt; Vue component&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When everything was set up properly I started seeing ads on my site, and I see a non-zero value in my estimated earnings in the AdSense console. Google has a payment threshold of $100, so I need make this amount before I can start receiving money from Google.&lt;/p&gt;

&lt;p&gt;My estimated earning report shows that I make between $0 and $4.32 in ad sales per day. I'm interested to see how much I can make with an article that I post across the many different channels that I can publish to. I explored this in a previous article, but the main channels I can use for sharing content are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DEV.to&lt;/li&gt;
&lt;li&gt;Facebook&lt;/li&gt;
&lt;li&gt;Hashnode&lt;/li&gt;
&lt;li&gt;Medium&lt;/li&gt;
&lt;li&gt;Reddit&lt;/li&gt;
&lt;li&gt;Discord&lt;/li&gt;
&lt;li&gt;Hacker Noon&lt;/li&gt;
&lt;li&gt;Twitter&lt;/li&gt;
&lt;li&gt;My MailChimp mailing list&lt;/li&gt;
&lt;li&gt;Substack&lt;/li&gt;
&lt;li&gt;Hacker News&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article should be a good place to start exploring how effective the different channels are in driving content to my site, and I'll update this article later with more details and numbers from my AdSense reports.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I built an AdBlock detector for my site
&lt;/h2&gt;

&lt;p&gt;I assume that most people reading my blog have an AdBlock extension installed in their browser like I do, such as AdBlock or ABP (AdBlock Pro). This got me thinking about how I could implement a simple AdBlock detector for my site that would hide the contents of the page if AdBlock is enabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  How do you check to see if AdBlock is enabled?
&lt;/h3&gt;

&lt;p&gt;I started with this question, and I came across &lt;a href="https://stackoverflow.com/questions/4869154/how-to-detect-adblock-on-my-website" rel="noopener noreferrer"&gt;this StackOverflow question&lt;/a&gt; which inspired the code that I am now using on this site to detect AdBlock.&lt;/p&gt;

&lt;h3&gt;
  
  
  The components of my AdBlock detector
&lt;/h3&gt;

&lt;p&gt;There are a few different parts of my Nuxt Application that work together to detect if AdBlock is active and request that the user pause AdBlock for the site. The main components are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A component called &lt;code&gt;AdBlockBlocker&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;This component is used in the default layout, so it is included in all pages on briancaffey.github.io&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Vuex store module called &lt;code&gt;adblock&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;this module is used to keep track of a boolean value that indicates if AdBlock is enabled&lt;/li&gt;
&lt;li&gt;the module also has a simple getter and a mutation for turning adBlock to true or false&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Some logic in the &lt;code&gt;default.vue&lt;/code&gt; layout that is used for almost all of the pages on my site

&lt;ul&gt;
&lt;li&gt;the getter from the Vuex store is used here to either show regular content or a message that the user needs to pause AdBlock&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;A component/page to display when AdBlock is enabled

&lt;ul&gt;
&lt;li&gt;this component asks the user to please pause AdBlock&lt;/li&gt;
&lt;li&gt;I named this component &lt;code&gt;PleaseDisableAdblock.vue&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;localStorage

&lt;ul&gt;
&lt;li&gt;this is used to keep track of the presence of an AdBlocker that is blocking ads&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's an overview of each part:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AdBlockBlocker.vue&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the key part of how the AdBlock detection works. If the client is unable to download the &lt;code&gt;adsbygoogle.js&lt;/code&gt; file, then that indicates that the user is using AdBlock.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;mounted &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// if adblock is detected through a value that is set local storage, then show the AdBlock message&lt;/span&gt;
    &lt;span class="c1"&gt;// the `adblockEnabled` value is set in the catch block of the `detectAdBlock` method&lt;/span&gt;
    &lt;span class="c1"&gt;// (see adblock/setAdblockEnabled mutation)&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adblockEnabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adblock/setAdblockEnabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c1"&gt;// check to see if the URL can be accessed on a 5 second interval&lt;/span&gt;
    &lt;span class="nf"&gt;setInterval&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detectAdBlock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt;  &lt;span class="nf"&gt;detectAdBlock &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// this is a URL that should be blocked by AdBlock&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;googleAdUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
      &lt;span class="c1"&gt;// make a request to the above URL&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;googleAdUrl&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// isAdblockEnabled is false by default&lt;/span&gt;
        &lt;span class="c1"&gt;// Check to see if isAblockEnabled was set to true by a previous request&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adblock/isAdblockEnabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// if the request was successful, then the user does not have AdBlock enabled,&lt;/span&gt;
          &lt;span class="c1"&gt;// so we can set isAdblockEnabled to false using the setAdblockEnabled mutation&lt;/span&gt;
          &lt;span class="c1"&gt;// this mutation will also set the `adblockEnabled` value in local storage to "false"&lt;/span&gt;
          &lt;span class="c1"&gt;// `adblockEnabled` be `JSON.parse`d since it is saved in localStorage as a string&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adblock/setAdblockEnabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adblockEnabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;false&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="c1"&gt;// do a full reload of the page&lt;/span&gt;
          &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reload&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="k"&gt;catch&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// if the request was unsuccessful, then the user has AdBlock enabled.&lt;/span&gt;
        &lt;span class="c1"&gt;// we can set isAdblockEnabled to true using the setAdblockEnabled mutation&lt;/span&gt;
        &lt;span class="c1"&gt;// this will also set the `adblockEnabled` value in local storage to "true"&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adblock/setAdblockEnabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adblockEnabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;true&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;script&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One other important point about this component is that it runs AdBlock detection using &lt;code&gt;setInterval&lt;/code&gt;, meaning that it will check if AdBlock is enabled every few seconds while a user is on my site.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;If AdBlock is enabled, the request will fail and the Vuex store value will be updated, which will cause the site to display the &lt;code&gt;PleaseDisableAdblock.vue&lt;/code&gt; component.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Id AdBlock is not enabled, then the file will be read from disk cache and the Vuex store value will remain &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Vuex Store&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is a very simple Vuex store module. The &lt;code&gt;isAdblockEnabled&lt;/code&gt; getter will be used in the &lt;code&gt;default.vue&lt;/code&gt; layout to component.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;initialValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;initialValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;adblockEnabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;adblockEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;initialValue&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;getters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;isAdblockEnabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adblockEnabled&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mutations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;setAdblockEnabled &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adblockEnabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;payload&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;default.vue layout logic&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vue"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Navigation&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;PleaseDisableAdblock&lt;/span&gt; &lt;span class="na"&gt;v-if=&lt;/span&gt;&lt;span class="s"&gt;"$store.getters['adblock/isAdblockEnabled']"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;Nuxt&lt;/span&gt; &lt;span class="na"&gt;v-else&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;AdBlockerBlocker&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;Footer&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="k"&gt;template&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Content to show to request that a user pauses AdBlock&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When users has AdBlock enabled, I show a simple message that asks them to please disable AdBlock. I also want to invite people who do not wish to disabled AdBlock to read my blog directly on GitHub, or to read it without ads by cloning or forking my repo, building it and running it in development mode with &lt;code&gt;yarn dev&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why am I adding ads and an AdBlock detector to my site?
&lt;/h2&gt;

&lt;p&gt;Being asked to pause AdBlock is increasingly common. I have noticed that the experience is not the same on each site that requests AdBlock be paused. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a site might ask you to pause AdBlock, but you have the option to continue without pausing AdBlock&lt;/li&gt;
&lt;li&gt;a site shows you a preview of an article and asks you to unpause AdBlock to see the full article&lt;/li&gt;
&lt;li&gt;all site content is hidden if you are using AdBlock, and a pop-up message ask you to "Continue to site" once you have paused AdBlock.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I remember being able to delete AdBlock detection modals and backgrounds on some sites as a way of getting around ad block detectors. This is as easy as &lt;code&gt;Cmd + Shift + C&lt;/code&gt;, click on the element that is blocking the page content and &lt;code&gt;delete&lt;/code&gt; the selected element.&lt;/p&gt;

&lt;p&gt;One of the reasons I wanted to implement AdBlock detection is to see what is possible from the perspective of user experience (UX). Ideally, here's how I want to the "please disable AdBlock" experience to work on my site:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A user visits my site with AdBlock enabled&lt;/li&gt;
&lt;li&gt;For a few seconds, the user can start reading the content of the article&lt;/li&gt;
&lt;li&gt;The page content is replaced with a message that says "Please disable AdBlock" and some other links that people may find interesting or helpful.&lt;/li&gt;
&lt;li&gt;The user goes into the AdBlock extension and pauses AdBlock on my site&lt;/li&gt;
&lt;li&gt;The original page content is then displayed with ads&lt;/li&gt;
&lt;li&gt;If the user re-enables AdBlock shortly after pausing it, then the "Please disable AdBlock" message should be displayed again&lt;/li&gt;
&lt;li&gt;If a u&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I don't want to ask the user to press a button or make the user think that they need to refresh the page. This is why I'm using &lt;code&gt;setInterval&lt;/code&gt;, I continuously make requests to the Google Ads JavaScript file that will be used to detect if AdBlock enabled.&lt;/p&gt;

&lt;p&gt;I'm happy to pause my AdBlock for smaller sites that ask me to, or for newspaper sites that are supported by advertising, and I'm assuming that people visiting my site will also be OK with pausing AdBlock as a way of thanking me for the work that goes into what I share on my blog.&lt;/p&gt;

&lt;p&gt;I'm mostly curious to see what happens to my site's traffic, and to see what impact it could have on the earnings I make from AdSense.&lt;/p&gt;

&lt;p&gt;Some of my questions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What will happen to the bounce rate if I request that AdBlock users pause AdBlock for my site?&lt;/li&gt;
&lt;li&gt;What is the most effective amount of time to wait before requesting that a new user pause AdBlock for my site&lt;/li&gt;
&lt;li&gt;What else can I include on my "Please Disable AdBlock" page to encourage new users to pause AdBlock on my site?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'll have a good follow-up article to share with numbers from my AdSense and Google analytics.&lt;/p&gt;

&lt;h3&gt;
  
  
  One small issue
&lt;/h3&gt;

&lt;p&gt;While ads are working on my site, I did notice that a console error related to AdSense:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;K {message: "adsbygoogle.push() error: Only one 'enable_page_level_ads' allowed per page.", name: 'TagError', pbr: true, stack: "TagError: adsbygoogle.push() error: Only one 'enab…agead/js/adsbygoogle.js?client=ca-google:77:1130)"}
message: "adsbygoogle.push() error: Only one 'enable_page_level_ads' allowed per page."
name: "TagError"
pbr: true
stack: "TagError: adsbygoogle.push() error: Only one 'enable_page_level_ads' allowed per page.
    at go (http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-google:219:326)
    at fo (http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-google:218:788)
    at mo (http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-google:225:365)
    at c (http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-google:226:38)
    at no (http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-google:226:156)
    at yo (http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-google:235:248)
    at oo (http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-google:232:89)
    at http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-google:227:47
    at Od.aa.ma (http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-google:64:802)
    at Jf (http://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-google:77:1130)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's a related issue on GitHub: &lt;a href="https://github.com/nuxt-community/google-adsense-module/issues/141" rel="noopener noreferrer"&gt;https://github.com/nuxt-community/google-adsense-module/issues/141&lt;/a&gt;. I'm still not sure how to fix this issue. If anyone has any ideas, please let me know!&lt;/p&gt;

&lt;p&gt;If you are interested in following my progress, feel free to subscribe to my MailChimp newsletter by filling out the form in the footer of my website, or by following me on any of the accounts listed on &lt;a href="https://briancaffey.github.io/contact" rel="noopener noreferrer"&gt;briancaffey.github.io/contact&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>adsense</category>
      <category>adblock</category>
      <category>ads</category>
      <category>nuxt</category>
    </item>
    <item>
      <title>How I write and share technical software development articles in 2021</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Sat, 02 Oct 2021 00:00:00 +0000</pubDate>
      <link>https://dev.to/briancaffey/how-i-write-and-share-technical-software-development-articles-in-2021-27n2</link>
      <guid>https://dev.to/briancaffey/how-i-write-and-share-technical-software-development-articles-in-2021-27n2</guid>
      <description>&lt;p&gt;This article describes how I write and share technical articles on my personal website and other developer websites and technical article aggregation sites.&lt;/p&gt;

&lt;p&gt;This article is broken into three sections:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;How I use Nuxt.js to build &lt;a href="https://briancaffey.github.io" rel="noopener noreferrer"&gt;my personal website&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;How I share my articles on other development sites and article aggregators&lt;/li&gt;
&lt;li&gt;Bonus content, project plug and conclusion&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Building &lt;code&gt;briancaffey.github.io&lt;/code&gt; with Nuxt.js
&lt;/h2&gt;

&lt;p&gt;I first started writing my personal website on GitHub pages using a static website builder called &lt;a href="https://jekyllrb.com/docs/github-pages/" rel="noopener noreferrer"&gt;Jekyll&lt;/a&gt;. Jekyll is a great tool for getting started with building a personal portfolio or technical blog, and it served me well for several years. I eventually changed my static site generation tool from Jekyll to Nuxt since I wanted to learn more about Vue.js. Also, I don't know Ruby very well, and it was difficult for me to use the Jekyll template language.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Pages
&lt;/h3&gt;

&lt;p&gt;I host my website on GitHub pages at the following domain: &lt;a href="https://briancaffey.github.io" rel="noopener noreferrer"&gt;briancaffey.github.io&lt;/a&gt;. GitHub pages is a great way to host a public site. The subdomain, &lt;code&gt;briancaffey&lt;/code&gt; in my case, is your GitHub username. GitHub pages will serve content from a specified branch and nested folder. My website uses the branch &lt;code&gt;gh-pages&lt;/code&gt; and the root folder &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Actions
&lt;/h3&gt;

&lt;p&gt;I use GitHub actions to deploy my website to GitHub pages. Here's the file that sets up the GitHub action that builds and deploys my site:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github pages&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;master&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v2&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Node&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;14"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cache dependencies&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.npm&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ runner.os }}-node-&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn lint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;yarn generate&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;peaceiris/actions-gh-pages@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;github_token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;publish_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./docs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When changes are pushed to the &lt;code&gt;master&lt;/code&gt; branch, this GitHub Action runs. It lints the code, builds the site with &lt;code&gt;yarn generate&lt;/code&gt; and then the &lt;code&gt;peaceiris/actions-gh-pages@v3&lt;/code&gt; GitHub Action commits only the build artifacts to the &lt;code&gt;gh-pages&lt;/code&gt; branch where the content is served on &lt;code&gt;briancaffey.github.io&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nuxt.js Framework
&lt;/h3&gt;

&lt;p&gt;Nuxt.js is a versatile Vue.js framework. It can be used to build several different types of websites, including Server Side Rendered (SSR) websites, single page applications (SPA) and static websites. I use the static website mode, also called Server Side Generation (SSG).&lt;/p&gt;

&lt;h3&gt;
  
  
  Content API
&lt;/h3&gt;

&lt;p&gt;Nuxt has a module called &lt;a href="https://nuxtjs.org/api/content-api" rel="noopener noreferrer"&gt;Content API&lt;/a&gt; that allows you to do the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write articles in Markdown&lt;/li&gt;
&lt;li&gt;Use Vue components in Markdown&lt;/li&gt;
&lt;li&gt;write custom front-matter that can be used to query Markdown articles using an API&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Articles and Folder Structure
&lt;/h3&gt;

&lt;p&gt;Nuxt uses a folder structure that automatically generates routes and helps you organize components, pages and layouts. Here is the folder structure for my personal website's repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tree -L 1 .
.
├── README.md
├── assets  &amp;lt;-- compiled assets
├── components &amp;lt;-- Vue components used in the site
├── content  &amp;lt;-- contains Markdown files for articles
├── i18n  &amp;lt;-- contains translations
├── jsconfig.json
├── layouts  &amp;lt;-- layouts used for each page
├── middleware  &amp;lt;-- I'm not using this folder
├── node_modules
├── nuxt.config.js  &amp;lt;-- config file
├── package.json
├── pages  &amp;lt;-- directory structure defines URLs for site pages
├── plugins  &amp;lt;-- plugins
├── static  &amp;lt;-- files in this folder are served as-is
├── store  &amp;lt;-- Vuex store
├── tailwind.config.js
└── yarn.lock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The content folder has the following structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tree -L 3 content
content/
├── 2016
│   └── 04
│       └── 07
│           └── my-article.md
├── 2017
│   └── 01
│      └── 01
│           └── my-other-article.md
...
└── projects
│   └── my-project.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will produce the following routes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/2016/04/07/my-article.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/2017/01/01/my-other-article.html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/projects/my-project.html&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the folder structure for the &lt;code&gt;pages&lt;/code&gt; directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tree -L 4 pages
pages
├── README.md
├── _year
│   └── _month
│       └── _day
│           └── _slug.vue
├── blog
│   ├── index.vue
│   └── tags
│       ├── _tag.vue
│       └── index.vue
├── confirm-subscription.vue
├── contact
│   └── index.vue
├── drafts
│   └── index.vue
├── index.vue
├── projects
│   ├── _slug.vue
│   └── index.vue
└── thank-you.vue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Directories in the &lt;code&gt;pages&lt;/code&gt; directory starting with an underscore (like &lt;code&gt;_year&lt;/code&gt;) can be used as URL parameters.&lt;/p&gt;

&lt;h3&gt;
  
  
  Markdown and Vue Components
&lt;/h3&gt;

&lt;p&gt;One nice feature of Nuxt and the Nuxt Content module is the ability to use Vue components directly in Markdown files. Here's an example of a Vue component in a Markdown file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Markdown with Vue components

This is some content

&amp;lt;my-component /&amp;gt;

This is more content.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vue components used in Markdown files must be included in the &lt;code&gt;components/global&lt;/code&gt; directory. Here's an article on my blog where I used Vue components to show interactive graphs: &lt;a href="https://briancaffey.github.io/2021/01/16/i-scraped-analyzed-and-generated-yc-companies-founders-and-work-at-a-startup-job-postings" rel="noopener noreferrer"&gt;https://briancaffey.github.io/2021/01/16/i-scraped-analyzed-and-generated-yc-companies-founders-and-work-at-a-startup-job-postings&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Images
&lt;/h3&gt;

&lt;p&gt;Images are an important part of the articles that I write. Each article has an optional cover image. I try to include a cover image for each of the articles I write. I mostly use Gimp to create the cover images.&lt;/p&gt;

&lt;p&gt;The cover image for this article was made with Inkscape. I have also used Gimp and Blender to generate images for my blog.&lt;/p&gt;

&lt;p&gt;Including images in the body of an article is pretty simple on my personal site. First I need to add the image to the &lt;code&gt;static&lt;/code&gt; directory, and then I can reference that image using the following syntax:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;![alt text](/static/path/to/my-image.png)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the image for this article:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F981thyfzftwgd4ygy7j8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F981thyfzftwgd4ygy7j8.png" alt="Writing and publishing dev articles" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Markdown front-matter
&lt;/h3&gt;

&lt;p&gt;Front-matter is a way to define metadata for a Markdown file. It is used to define the title, description, and other metadata that can be used to query articles.&lt;/p&gt;

&lt;p&gt;Here's whe front-matter for this article on my personal site &lt;a href="https://briancaffey.github.io/2021/10/02/sharing-a-dev-article-everywhere" rel="noopener noreferrer"&gt;briancaffey.github.io/2021/10/02/sharing-a-dev-article-everywhere&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;How I write and share technical development articles in &lt;/span&gt;&lt;span class="m"&gt;2021&lt;/span&gt;
&lt;span class="na"&gt;date&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;2021-10-02'&lt;/span&gt;
&lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;This article describes how to write and share technical articles in &lt;/span&gt;&lt;span class="m"&gt;2021&lt;/span&gt;
&lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nuxt&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vue&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;publishing&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;writing&lt;/span&gt;
&lt;span class="na"&gt;draft&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also use front-matter for the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenGraph meta tags &amp;amp; social sharing&lt;/li&gt;
&lt;li&gt;links to other outlets&lt;/li&gt;
&lt;li&gt;meta tags&lt;/li&gt;
&lt;li&gt;article tags&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Nuxt Sitemap
&lt;/h3&gt;

&lt;p&gt;Using the &lt;code&gt;@nuxt/sitemap&lt;/code&gt; module, it is easy to generate a sitemap.xml file. This generates a static file that is available on &lt;code&gt;/sitemap.xml&lt;/code&gt;. This file is used in the Google Search Console to tell Google which pages on my site should be indexed. This helps with SEO.&lt;/p&gt;

&lt;h3&gt;
  
  
  RSS Feed
&lt;/h3&gt;

&lt;p&gt;An RSS feed is configured using another official Nuxt module called &lt;code&gt;@nuxtjs/feed&lt;/code&gt;. This plugin generates an RSS feed for the site in XML. An RSS feed can be used to automatically publish articles to other sites, I'll show this in the next section of this article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Analytics
&lt;/h3&gt;

&lt;p&gt;Google Analytics is used to track site traffic and gives insights into what content is popular, where my users are visiting from, how long they spend browsing my site and other helpful metrics. It is likely that many readers of my site may have disabled Google Analytics in their browsers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Google Search Console
&lt;/h3&gt;

&lt;p&gt;Google Search Console is another tool that is helpful from an SEO perspective.&lt;/p&gt;

&lt;p&gt;Here is a report from Google Data Studio showing some some of the metrics that I use to analyze my site's traffic: &lt;a href="https://briancaffey.github.io/2021/10/02/how-i-write-and-share-technical-software-development-articles-in-2021#google-search-console" rel="noopener noreferrer"&gt;https://briancaffey.github.io/2021/10/02/how-i-write-and-share-technical-software-development-articles-in-2021#google-search-console&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  MailChimp
&lt;/h3&gt;

&lt;p&gt;I use MailChimp to build a newsletter audience. I'll be sending out a newsletter to my current audience with an update about this article. I wrote an article about how to set up MailChimp on Nuxt. I wrote an article on my blog about how I set up a form for guests to sign up for a newsletter using a MailChimp form: &lt;a href="https://briancaffey.github.io/zh/2020/10/10/how-to-add-email-signup-form-to-nuxt-site-with-mailchimp.html" rel="noopener noreferrer"&gt;https://briancaffey.github.io/zh/2020/10/10/how-to-add-email-signup-form-to-nuxt-site-with-mailchimp.html&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  formsubmit.co
&lt;/h3&gt;

&lt;p&gt;Site visitors can send me messages through an online form called &lt;a href="https://formsubmit.co" rel="noopener noreferrer"&gt;formsubmit.co&lt;/a&gt;. I include this form on my site's &lt;a href="https://briancaffey.github.io/contact" rel="noopener noreferrer"&gt;Contact&lt;/a&gt; page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drift
&lt;/h3&gt;

&lt;p&gt;Drift is a freemium service that allows site visitors to send me messages in real time. It's a great way to get in touch with site visitors, and it can be configured so that messages go to the Drift mobile app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Drafts
&lt;/h3&gt;

&lt;p&gt;One option in the front-matter for my blog articles is &lt;code&gt;draft&lt;/code&gt;. If an article has &lt;code&gt;draft: true&lt;/code&gt; set in the front-matter, then the article will not be listed on the main list of blog articles on my site, and the page will not be indexed by Google. Here's where you can find the draft articles for my site: &lt;a href="https://briancaffey.github.io/drafts" rel="noopener noreferrer"&gt;https://briancaffey.github.io/drafts&lt;/a&gt;. The articles here can be accessed publicly, but I only show them on the &lt;code&gt;/drafts&lt;/code&gt; page which is not listed anywhere else on my site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Publishing on other outlets
&lt;/h2&gt;

&lt;p&gt;When publishing articles from my personal website on other sites, I make sure that custom content is either removed or replaced with a link or static image that I can upload to the other site while editing the article.&lt;/p&gt;

&lt;p&gt;For example, this article includes an embedded Google Data Studio report in the version that is published on &lt;code&gt;briancaffey.github.io&lt;/code&gt;. This embedded iframe will not work when posted to other platforms, so I can instead link to an anchor tag that corresponds to the location of the custom element on my site.&lt;/p&gt;

&lt;p&gt;For this article, I have mostly tried to keep the custom content to a minimum so that it will be easy to cross publish on other sites without having to make lots of edits to the markdown. Most of the tweaking will likely have to do with preview images and other custom front-matter properties that some site (like DEV.to) support.&lt;/p&gt;

&lt;h3&gt;
  
  
  DEV.to
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://dev.to"&gt;DEV.to&lt;/a&gt; is a popular site for sharing technical articles. They allow you to automatically draft articles to publish on their site by adding your site's RSS feed. This article will be published on DEV.to through the RSS feed connection that my account has with DEV.to.&lt;/p&gt;

&lt;p&gt;DEV.to articles support their own custom front-matter properties. Here's what the front-matter for the DEV.to article looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
title: How I write and share technical software development articles in 2021
published: false
date: '2021-10-02'
tags:  nuxt, vue, publishing, blogging
image: 'https://briancaffey.github.io/static/dev-sites.png'
canonical_url: https://briancaffey.github.io/2021/10/02/how-i-write-and-share-technical-software-development-articles-in-2021
---
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't see your article in your list of article drafts on your DEV.to dashboard, you can go into &lt;code&gt;Settings &amp;gt; Extensions &amp;gt; Publishing to DEV Community from RSS&lt;/code&gt; and click on &lt;code&gt;Save Feed Settings&lt;/code&gt;. I think this refreshes your RSS feed in your dashboard.&lt;/p&gt;

&lt;h3&gt;
  
  
  Medium
&lt;/h3&gt;

&lt;p&gt;I haven't published anything on Medium, so one of my goals for this article is to cross publish it on Medium in my first article on that platform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://medium.com/new-story" rel="noopener noreferrer"&gt;https://medium.com/new-story&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Hashnode
&lt;/h3&gt;

&lt;p&gt;Hashnode seems very similar to DEV.to. Here's a comparison that shows some of the advantages of using Hashnode as a blogging platform over DEV.to: &lt;a href="https://hashnode.com/vs/devto" rel="noopener noreferrer"&gt;https://hashnode.com/vs/devto&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://hashnode.com/create/story" rel="noopener noreferrer"&gt;https://hashnode.com/create/story&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  egghead.io
&lt;/h3&gt;

&lt;p&gt;Egghead is another blogging platform that allows you to helps you &lt;code&gt;Own Your Online Presence&lt;/code&gt; and also lets you create free and paid courses and content.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hacker Noon
&lt;/h3&gt;

&lt;p&gt;Hacker Noon is another platform that I haven't used before as a writer, but one that I'm aiming to publish on with this article!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://app.hackernoon.com/new" rel="noopener noreferrer"&gt;https://app.hackernoon.com/new&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Reddit
&lt;/h3&gt;

&lt;p&gt;I have shared a lot of content on different programming subreddits specific to some of the tools and frameworks I use, such as &lt;code&gt;r/aws&lt;/code&gt; and &lt;code&gt;r/django&lt;/code&gt;. When sharing on reddit, I like to share links to my personal website with at least on comment that provides a detailed summary of the article. When sharing on&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.reddit.com/r/Nuxt/" rel="noopener noreferrer"&gt;https://www.reddit.com/r/Nuxt/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Facebook
&lt;/h3&gt;

&lt;p&gt;Facebook has very large and active developer communities. Sometimes the communities are more fragmented than the communities on reddit. For example, there are several Nuxt communities on Facebook, but there is just one &lt;code&gt;r/nuxt&lt;/code&gt;. Similar to sharing content on reddit, I like to share links to my personal websites with detailed comments on the content of my article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Discord Servers
&lt;/h3&gt;

&lt;p&gt;Discord also has some dedicated servers for software frameworks, such as Nuxt.js. Discord seems to be the official place where Vue.js community members chat in real-time. There are dedicated channels on the server for sharing articles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bonus content, project plug and conclusion
&lt;/h2&gt;

&lt;p&gt;One more great thing about GitHub pages is that you can publish a site on any of your GitHub repositories that will be hosted on a subpath of your GitHub pages blog.&lt;/p&gt;

&lt;p&gt;I have been working updating and rewriting my Django + Vue.js + AWS reference project. It contains a documentation site that I am making with VuePress. The repo for this project is here: &lt;a href="https://github.com/briancaffey/django-step-by-step" rel="noopener noreferrer"&gt;github.com/briancaffey/django-step-by-step&lt;/a&gt;. This repository has it's own GitHub Pages configuration, as well as a GitHub Action to help automate the deployment of this project documentation site to GitHub Pages. The project site is currently hosted on &lt;a href="https://briancaffey.github.io/django-step-by-step/" rel="noopener noreferrer"&gt;briancaffey.github.io/django-step-by-step/&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You may want to split a large project's documentation site into its own site, rather than having it live on the nested path of a personal blog. Following this pattern, your GitHub pages blog can become a site that is much larger than one single Nuxt static site. &lt;code&gt;briancaffey.github.io&lt;/code&gt; is now a hybrid Nuxt.js and VuePress site, with a subset of routes (starting with /django-step-by-step/) being served by VuePress.&lt;/p&gt;

&lt;p&gt;As I'm writing this article, Nuxt 3 is almost one week away from a public beta. I'm excited to try upgrading this site to Nuxt 3 and trying out some of the new features that it includes.&lt;/p&gt;

&lt;p&gt;Thanks for reading this article, wherever you may have found it on the internet!&lt;/p&gt;

</description>
      <category>nuxt</category>
      <category>vue</category>
      <category>publishing</category>
      <category>blogging</category>
    </item>
    <item>
      <title>On-demand, serverless Valheim server setup with AWS CDK, Discord Interactions and GitLab CI</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Sat, 20 Mar 2021 17:07:51 +0000</pubDate>
      <link>https://dev.to/briancaffey/on-demand-serverless-valheim-server-setup-with-aws-cdk-discord-interactions-and-gitlab-ci-58pj</link>
      <guid>https://dev.to/briancaffey/on-demand-serverless-valheim-server-setup-with-aws-cdk-discord-interactions-and-gitlab-ci-58pj</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Here's a link to the GitLab repo I'll be referencing in this article: &lt;a href="https://gitlab.com/briancaffey/valheim-cdk-discord-interactions" rel="noopener noreferrer"&gt;https://gitlab.com/briancaffey/valheim-cdk-discord-interactions&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is an in-depth technical article about running an on-demand, dedicated server for Valheim using Amazon Web Services controlled with Discord Slash Commands, a new part their Interactions API that is currently in beta. Valheim is an open-world online multiplayer survival game loosely based on Norse mythology that has blown up recently.&lt;/p&gt;

&lt;p&gt;My main goal with this project was to find an inexpensive way of running a server given how my friends and I play the game, which is typically a few times a week in the evenings. Some combination of 4 of us will start playing, and we drop in and out between walking dogs, cooking, etc. When playing, we all jump on a dedicated voice channel on our group's Discord.&lt;/p&gt;

&lt;p&gt;Before setting up a dedicated server, our game's world state was stored on files that lived on one of our computers, and that computer needed to be running the game server in order for anyone to connect. Sending files around would be possible, but would quickly become tedious. There are lots of services that offer dedicated servers for Valheim, as well as many technical guides and channels on the official Valheim Discord server to support the use of dedicated servers. I wanted to see if I could set up a server myself on AWS using CDK, or Cloud Development Kit. CDK is an Infrastructure as Code (IaC) tool that allows you to define, deploy and update AWS infrastructure with popular programming languages such as Python, Typescript, Java, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;cdk-valheim&lt;/code&gt; construct on GitHub
&lt;/h2&gt;

&lt;p&gt;The best part of CDK is that it enables the creation high-level, reusable constructs that can be published to software registries like npm and PyPI. Developers can import and use these constructs in their own CDK code. A quick google search for "cdk valheim" turned up a few results. &lt;a href="https://github.com/gotodeploy/cdk-valheim" rel="noopener noreferrer"&gt;cdk-valheim&lt;/a&gt; seems like the best option for what I was looking for. This project uses ECS, a container orchestration tool from AWS that I have experience using with web applications and EFS for persistent file storage. Although it is written in Typescript, I can still use the construct in my preferred programming language (Python) without any extra effort or configuration. This is thanks to the jsii. From &lt;a href="https://github.com/aws/jsii" rel="noopener noreferrer"&gt;https://github.com/aws/jsii&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;jsii&lt;/code&gt; allows code in any language to naturally interact with JavaScript classes. It is the technology that enables the AWS Cloud Development Kit to deliver polyglot libraries from a single codebase!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's an overview of the &lt;code&gt;cdk-valheim&lt;/code&gt; construct:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scheduled scaling of an ECS service using AWS Fargate (a serverless compute engine for containers)&lt;/li&gt;
&lt;li&gt;Elastic File System (EFS) file system mounted into the Fargate Task container of our ECS service&lt;/li&gt;
&lt;li&gt;Optional automated backups of the EFS file system using AWS Backup&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Discord Interactions and Slash Commands
&lt;/h2&gt;

&lt;p&gt;Scheduled ECS scaling is nice, but we don't always know when we will be able to play together, so it is not the best way to minimize infrastructure costs. My plan was to set the ECS service to an initial task count of zero and then let any of us set the number of tasks to either zero or one through a Discord Slash Command. A Slash Command allows you to interact with a discord bot by typing &lt;code&gt;/&lt;/code&gt; and then tabbing through to the option we want, such as &lt;code&gt;/valheim server start&lt;/code&gt; or &lt;code&gt;/valheim server status&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Invoking a Slash Command from Discord sends a &lt;code&gt;POST&lt;/code&gt; request from Discord to a webhook URL that we have to provide. To handle the webhook, one simple and inexpensive approach is to use API Gateway and a Lambda function that serves a Flask app. The Flask app can then use boto3 (which is included in the Lambda execution environment) to call &lt;code&gt;update_service&lt;/code&gt; or &lt;code&gt;describe_services&lt;/code&gt; to scaled the ECS task's &lt;code&gt;desiredCount&lt;/code&gt; based on the slash command options and sub options.&lt;/p&gt;

&lt;p&gt;The function that handles the webhook &lt;code&gt;POST&lt;/code&gt; request when the sub-command is &lt;code&gt;status&lt;/code&gt; queries AWS for the number of ECS tasks in our service that are &lt;code&gt;desired&lt;/code&gt;, &lt;code&gt;running&lt;/code&gt; and &lt;code&gt;pending&lt;/code&gt; and then sends back a message that will be displayed to the user who sent the command. When the sub-command is &lt;code&gt;start&lt;/code&gt; or &lt;code&gt;stop&lt;/code&gt;, the &lt;code&gt;desiredCount&lt;/code&gt; is either set to &lt;code&gt;1&lt;/code&gt; or &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here's an overview of the server setup and how Discord Slash Commands can be used to control the server:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd66t3cbd0n9vxeem6qyh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd66t3cbd0n9vxeem6qyh.png" alt="Diagram" width="800" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'll cover each of the steps labelled in this diagram at the end of the article. The rest of the article will provide detailed instructions for how to set everything up. I won't be going over AWS account setup or Discord server setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to set up the Discord developer application and Interaction
&lt;/h2&gt;

&lt;p&gt;First, you need to be the admin of a Discord server. Once you create the server, go to &lt;code&gt;Server Settings &amp;gt; Widget&lt;/code&gt; and take note of the Server ID. This is also known as the Guild ID.&lt;/p&gt;

&lt;p&gt;Then go to &lt;a href="https://discord.com/developers/applications" rel="noopener noreferrer"&gt;https://discord.com/developers/applications&lt;/a&gt; and create an application. Under &lt;code&gt;General Information&lt;/code&gt;, make note of the public key (the application ID). Put these values in a file that will be &lt;code&gt;.gitignored&lt;/code&gt; called &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export GUILD_ID=123456789
export APPLICATION_ID=abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will use this file later when registering the Interaction.&lt;/p&gt;

&lt;p&gt;Next go to the &lt;code&gt;OAuth2&lt;/code&gt; tab and select the &lt;code&gt;bot&lt;/code&gt; and &lt;code&gt;applications.commands&lt;/code&gt; permissions. This will generated an OAuth2 authorization link. Copy the link and open it in a browser. We will see an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OAuth2 application does not have a bot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure a bot for our Discord application
&lt;/h3&gt;

&lt;p&gt;Next, got to the &lt;code&gt;Bot&lt;/code&gt; tab and click the &lt;code&gt;Add Bot&lt;/code&gt; button.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Adding a bot user gives your app visible life in Discord. However, this action is irrevocable! Choose wisely.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Turn off the &lt;code&gt;Public Bot&lt;/code&gt; option and save changes.&lt;/p&gt;

&lt;p&gt;Get the bot token by clicking on &lt;code&gt;Click to Reveal Token&lt;/code&gt;, and add this to &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export BOT_TOKEN=abc.xyz.123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now go back to the &lt;code&gt;OAuth2&lt;/code&gt; tab and select the &lt;code&gt;bot&lt;/code&gt; and &lt;code&gt;applications.commands&lt;/code&gt; permissions again, copy the link and open it. Select the server that you want to add this application to. You should see a captcha, and then a message that says &lt;code&gt;Authorized&lt;/code&gt;. You should also see a message from your discord server that the bot has joined the server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the Interaction
&lt;/h3&gt;

&lt;p&gt;Now we will set up the Interaction. Currently the only way to set up the interaction is through an HTTP &lt;code&gt;POST&lt;/code&gt; request. This Python script sets up our Interaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
https://discord.com/developers/docs/interactions/slash-commands#registering-a-command
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;APPLICATION_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APPLICATION_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;GUILD_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GUILD_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;BOT_TOKEN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BOT_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://discord.com/api/v8/applications/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;APPLICATION_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/guilds/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GUILD_ID&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/commands&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;vh&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Start, stop or get the status of the Valheim server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valheim_server_controls&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;What do you want to do?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;choices&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bot &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BOT_TOKEN&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before running this command, source the &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 register_bot.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see this response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;b'{"id": "XXXXXXXXXXXXXX", "application_id": "XXXXXXXXXXXXXX", "name": "vh", "description": "Start, stop or get the status of the Valheim server", "version": "XXXXXXXXXXXXXX", "default_permission": true, "guild_id": "XXXXXXXXXXXXXX", "options": [{"type": 3, "name": "valheim_server_controls", "description": "What do you want to do?", "required": true, "choices": [{"name": "status", "value": "status"}, {"name": "start", "value": "start"}, {"name": "stop", "value": "stop"}]}]}'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when you type &lt;code&gt;/&lt;/code&gt; in any channel on the Discord server that you authenticated the bot, you should see the &lt;code&gt;vh&lt;/code&gt; command at the top of the list of autocomplete options.&lt;/p&gt;

&lt;p&gt;If we run any of these commands, we should see a response saying:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This interaction failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is because we have not configured an &lt;code&gt;Interactions Endpoint URL&lt;/code&gt; under the &lt;code&gt;General Information&lt;/code&gt; section of our Discord Application's admin page (&lt;a href="https://discord.com/developers/applications/" rel="noopener noreferrer"&gt;https://discord.com/developers/applications/&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the Interactions Endpoint URL for our Slash Command
&lt;/h2&gt;

&lt;p&gt;In order for our Slash Command to do anything, we need to set up URL that Discord will &lt;code&gt;POST&lt;/code&gt; the Interaction event data to, including information such as who sent the Interaction, what channel it was sent on, what options were used, etc. You can see an example of the event payload &lt;a href="https://discord.com/developers/docs/interactions/slash-commands#receiving-an-interaction" rel="noopener noreferrer"&gt;here on the Discord developer documentation&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"token"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A_UNIQUE_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"member"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;53908232506183680&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"username"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mason"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"avatar"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"a_d5efa99b3eeaa7dd43acca82f5692432"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"discriminator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1337"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"public_flags"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;131141&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"roles"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"539082325061836999"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"premium_since"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"permissions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2147483647"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"pending"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"nick"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"mute"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"joined_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2017-03-13T19:19:14.040000+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"is_pending"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"deaf"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"786008729715212338"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"guild_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"290926798626357999"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cardname"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The Gitrog Monster"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cardsearch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"771825006014889984"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"channel_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"645027906669510667"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This &lt;code&gt;POST&lt;/code&gt; request also includes some special headers used for security that we will need to do validation with our handling function. This part can be handled with a decorator provided by the &lt;code&gt;discord-interactions&lt;/code&gt; package on PyPI, but we will need to add some additional configuration to our API Gateway endpoint since these headers will not be passed through the lambda by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up AWS infrastructure with CDK
&lt;/h2&gt;

&lt;p&gt;Let's start a CDK project in a blank repository that will define our infrastructure, Lambda functions and CI/CD pipeline with GitLab CI. Make sure that you have the &lt;code&gt;aws-cdk&lt;/code&gt; CLI installed globally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npm i -g aws-cdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then start a CDK project in a subdirectory called &lt;code&gt;cdk&lt;/code&gt; with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir cdk &amp;amp;&amp;amp; cd cdk &amp;amp;&amp;amp; cdk init app --language=python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Add CDK project dependencies
&lt;/h3&gt;

&lt;p&gt;The next step is to add all of the dependencies to our CDK project that we will use in this project. In &lt;code&gt;setup.py&lt;/code&gt; add the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;    &lt;span class="n"&gt;install_requires&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws-cdk.core==1.92.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws-cdk.aws_applicationautoscaling==1.92.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws-cdk.aws_datasync==1.92.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws-cdk.aws_lambda==1.92.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws-cdk.aws_s3==1.92.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aws-cdk.aws_apigateway==1.92.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cdk-valheim==0.0.16&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next we can add the CDK construct for &lt;code&gt;ValheimWorld&lt;/code&gt; in the &lt;code&gt;cdk_stack.py&lt;/code&gt; file that was generated in our project as well as the imports for the packages we included in &lt;code&gt;setup.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_cdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cdk&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_cdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_datasync&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;datasync&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_iam&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_lambda&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_apigateway&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_applicationautoscaling&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;appScaling&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;aws_s3&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cdk_valheim&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ValheimWorld&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ValheimWorldScalingSchedule&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CdkStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stack&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Construct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;construct_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;construct_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# The code that defines your stack goes here
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valheim_world&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ValheimWorld&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ValheimWorld&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;memory_limit_mib&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;schedules&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;ValheimWorldScalingSchedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;appScaling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CronOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;12&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;week_day&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1-5&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;appScaling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CronOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;week_day&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1-5&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;)],&lt;/span&gt;
            &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERVER_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERVER_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CDK Valheim&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WORLD_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WORLD_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Amazon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERVER_PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SERVER_PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fargate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BACKUPS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;false&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We are almost ready to deploy a basic version of our Valheim server using the &lt;code&gt;cdk-valheim&lt;/code&gt; construct. If we were to run the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdk deploy --app cdk/app.py --require-approval never
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;from the root of our project, it should work. This assumes that we have default credentials configured in &lt;code&gt;~/.aws/credentials&lt;/code&gt; and that we have also bootstrapped our AWS account with the resources it needs for CDK to work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cdk bootstrap --app cdk/app.py aws://$AWS_ACCOUNT_ID/$AWS_DEFAULT_REGION
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setup GitLab CI job for automated deployments
&lt;/h3&gt;

&lt;p&gt;Instead of deploying from the command line, it would be better to run the deployment from a CI/CD pipeline. Add a &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; file to the root of your project and populate it with the following YAML:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3.8&lt;/span&gt;

&lt;span class="na"&gt;cdk_deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_TAG"&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
  &lt;span class="na"&gt;before_script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apt-get -qq update &amp;amp;&amp;amp; apt-get -y install nodejs npm&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm i -g aws-cdk&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip3 install -e cdk&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cdk bootstrap --app cdk/app.py aws://$AWS_ACCOUNT_ID/$AWS_DEFAULT_REGION&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cdk deploy --app cdk/app.py --require-approval never&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before we initialize a git repository in the root directory of our project, remove the &lt;code&gt;.git&lt;/code&gt; repo that CDK created when we initialized the project with &lt;code&gt;rf -rf cdk/.git&lt;/code&gt;. Now initialized a project in the root directory with &lt;code&gt;git init&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, create a GitLab repository and add the remote to this project with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git remote add origin git@gitlab.com:gitlab-username/project-name.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the GitLab project's &lt;code&gt;Settings &amp;gt; CI/CD &amp;gt; Variables&lt;/code&gt; section, add the following environment variables as protected variables:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AWS_DEFAULT_REGION&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AWS_ACCOUNT_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;APPLICATION_PUBLIC_KEY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SERVER_PASS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;SERVER_NAME&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;WORLD_NAME&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now under &lt;code&gt;Settings &amp;gt; Repository &amp;gt; Protected Tags&lt;/code&gt;, add a wildcard (&lt;code&gt;*&lt;/code&gt;) so that all tags are protected and only maintainers can push tags. This allows us to use the protected environment variables only when a trusted maintainer pushes a tag to the repository.&lt;/p&gt;

&lt;h3&gt;
  
  
  Edit the name of the stack and add region and account info
&lt;/h3&gt;

&lt;p&gt;We are almost ready to create a tag and push to GitLab, but before we do that let's change the name of the CloudFormation stack that CDK will create in &lt;code&gt;cdk/app.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_cdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;cdk&lt;/span&gt;

&lt;span class="c1"&gt;# For consistency with TypeScript code, `cdk` is the preferred import name for
# the CDK's core module.  The following line also imports it as `core` for use
# with examples from the CDK Developer's Guide, which are in the process of
# being updated to use `cdk`.  You may delete this import if you don't need it.
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;aws_cdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;core&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;cdk.cdk_stack&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CdkStack&lt;/span&gt;

&lt;span class="n"&gt;aws_region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_DEFAULT_REGION&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us-east-1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;aws_account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AWS_ACCOUNT_ID&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;App&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nc"&gt;CdkStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;valheim-server-stack&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;aws_region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;aws_account&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now commit changes, create a tag and push it to GitLab:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;git add .
git commit -m "initial commit"
git tag v0.0.1
git push origin v0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the logs of the GitLab CI pipeline that this creates in your GitLab project's CI/CD settings.&lt;/p&gt;

&lt;p&gt;If everything runs successfully, you should be able to see your Valheim server listed in the list of community servers once it comes online, and you should be able to connect to it with the password you set in GitLab project variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the Lambda function handler code
&lt;/h2&gt;

&lt;p&gt;We will have a simple Flask application respond the the Discord &lt;code&gt;POST&lt;/code&gt; requests that send Interaction events. Let's add &lt;code&gt;lambda-handler.py&lt;/code&gt; in &lt;code&gt;lambda/functions/interactions/lambda-handler.py&lt;/code&gt;, and &lt;code&gt;requirements.txt&lt;/code&gt; in &lt;code&gt;lambda/functions/interactions/requirements.txt&lt;/code&gt;. Our project structure should look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ tree -L 4
.
├── cdk
│   ├── app.py
│   ├── cdk
│   │   ├── cdk_stack.py
│   │   └── __init__.py
│   ├── cdk.json
│   ├── README.md
│   ├── requirements.txt
│   ├── setup.py
│   └── source.bat
├── lambda
│   └── functions
│       └── interactions
│           ├── lambda-handler.py    &amp;lt;-- here
│           └── requirements.txt     &amp;lt;-- and here
├── README.md
└── register_bot.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;requirements.txt&lt;/code&gt; defines the pip dependencies for our Lambda function. It should include the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;aws-wsgi==0.2.7
discord-interactions==0.2.0
Flask==1.1.2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;aws-wsgi&lt;/code&gt; will transform API Gateway requests into WSGI application requests that Flask can handle&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;discord-interactions&lt;/code&gt; will help us with some security-related requirements&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Flask&lt;/code&gt; will be our web application framework&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the code for &lt;code&gt;lambda-handler.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;awsgi&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;discord_interactions&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;verify_key_decorator&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ecs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Your public key can be found on your application in the Developer Portal
&lt;/span&gt;&lt;span class="n"&gt;PUBLIC_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;APPLICATION_PUBLIC_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getLogger&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLevel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/discord&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="nd"&gt;@verify_key_decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PUBLIC_KEY&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;index&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;interaction_option&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;options&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Could not parse the interaction option&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;interaction_option&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Interaction:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interaction_option&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;interaction_option&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

                &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;describe_services&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ECS_CLUSTER_ARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
                        &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ECS_SERVICE_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                    &lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;desired_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;services&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;desiredCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;running_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;services&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;runningCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;pending_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;services&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pendingCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

                &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Desired: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;desired_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | Running: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;running_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; | Pending: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pending_count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;Error&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Could not get server status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Could not get the server status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;interaction_option&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Starting the server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ECS_CLUSTER_ARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ECS_SERVICE_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;desiredCount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;interaction_option&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stop&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stopping the server&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ECS_CLUSTER_ARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ECS_SERVICE_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="n"&gt;desiredCount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown command&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;embeds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed_mentions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;awsgi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;base64_content_types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice how we pass the Flask &lt;code&gt;app&lt;/code&gt; to &lt;code&gt;awsgi.response&lt;/code&gt;. &lt;code&gt;aws-wsgi&lt;/code&gt; (or &lt;code&gt;awsgi&lt;/code&gt; as it is imported) is the go-between for API Gateway and WSGI.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add the CDK code for API Gateway and Lambda that will serve our Discord Interaction Endpoint URL
&lt;/h2&gt;

&lt;p&gt;Now we can add the following code to &lt;code&gt;cdk_stack.py&lt;/code&gt; to configure the API Gateway and Lambda function. Add the following to &lt;code&gt;cdk_stack.py&lt;/code&gt; after our definition of &lt;code&gt;self.valheim_world&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env_vars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APPLICATION_PUBLIC_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;APPLICATION_PUBLIC_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ECS_SERVICE_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valheim_world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ECS_CLUSTER_ARN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;valheim_world&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cluster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cluster_arn&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flask_lambda_layer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LayerVersion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FlaskAppLambdaLayer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AssetCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./layers/flask&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;compatible_runtimes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PYTHON_3_8&lt;/span&gt;&lt;span class="p"&gt;,],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flask_app_lambda&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FlaskAppLambda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;runtime&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PYTHON_3_8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AssetCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;./lambda/functions/interactions&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;function_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;flask-app-handler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;handler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lambda-handler.handler&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;layers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flask_lambda_layer&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;environment&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;env_vars&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flask_app_lambda&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_managed_policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;iam&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ManagedPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_managed_policy_arn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ECS_FullAccessPolicy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;managed_policy_arn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;arn:aws:iam::aws:policy/AmazonECS_FullAccess&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# https://slmkitani.medium.com/passing-custom-headers-through-amazon-api-gateway-to-an-aws-lambda-function-f3a1cfdc0e29
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_templates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'''&lt;/span&gt;&lt;span class="s"&gt;{
                &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$context.httpMethod&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
                &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; : $input.json(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;),
                &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: {
                    #foreach($param in $input.params().header.keySet())
                    &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$param&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$util.escapeJavaScript($input.params().header.get($param))&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
                    #if($foreach.hasNext),#end
                    #end
                }
            }
            &lt;/span&gt;&lt;span class="sh"&gt;'''&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apigateway&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;RestApi&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FlaskAppEndpoint&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apigateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ANY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;discord_interaction_webhook&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apigateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;discord&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;discord_interaction_webhook_integration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;apigw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;LambdaIntegration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flask_app_lambda&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;request_templates&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_templates&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;discord_interaction_webhook&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;discord_interaction_webhook_integration&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First we add some environment variables that will be made available to the Lambda function's execution environment. The ECS cluster and service name as well as our Discord application's &lt;code&gt;PUBLIC_KEY&lt;/code&gt; are needed in the Lambda function for everything to work.&lt;/p&gt;

&lt;p&gt;We have to give the lambda function permissions to make changes to ECS since it will be interacting with ECS via boto3.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;self.request_templates&lt;/code&gt; is needed in order to pass the special security headers from the Discord &lt;code&gt;POST&lt;/code&gt; request that are needed for security. I couldn't find a lot of resources on how to make this work, but I learned that this uses Apache Velocity Template Language.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add a GitLab CI job for installing dependencies into Lambda Layer
&lt;/h3&gt;

&lt;p&gt;There's one more step before we can push our code. We need to add another GitLab CI job that will install the Lambda dependencies so that they can be sent to the Lambda layer that we defined in our Lambda function. A Lambda Layer is where you install dependencies for this type of Lambda setup. Let's add the following stage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3.8&lt;/span&gt;

&lt;span class="na"&gt;pip_install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_TAG"&lt;/span&gt;
      &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;layers/flask/python&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip install -r lambda/functions/interactions/requirements.txt -t layers/flask/python&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we are installing dependencies into a target location (with the &lt;code&gt;-t&lt;/code&gt; flag) that our Lambda Layer will be able to use in the &lt;code&gt;cdk_deploy&lt;/code&gt; GitLab CI job. This is because we have indicated the path to &lt;code&gt;layers/flask/python&lt;/code&gt; in the &lt;code&gt;paths&lt;/code&gt; array of &lt;code&gt;artifacts&lt;/code&gt; in the &lt;code&gt;pip_install&lt;/code&gt; job. There are other ways to add the pip dependencies to the Lambda Layers. We don't absolutely need this to be done in a separate CI job.&lt;/p&gt;

&lt;p&gt;Now tag and push the code to GitLab and check to see that the pipeline runs successfully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Add the API Gateway URL to Discord Application settings
&lt;/h3&gt;

&lt;p&gt;If everything runs smoothly, we should see a URL in the very last lines of the pipeline. This is the URL for our API Gateway endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We need to add &lt;code&gt;discord&lt;/code&gt; to the end of this URL and then add that to our the &lt;code&gt;Interactions Endpoint URL&lt;/code&gt; in the &lt;code&gt;General Information&lt;/code&gt; section of our Discord application's admin page:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://abc123xyz.execute-api.us-east-1.amazonaws.com/prod/discord
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When we add this URL in the application settings, Discord will make sure that our endpoint is properly verifying the request based on its headers. Check out &lt;a href="https://github.com/discord/discord-interactions-python/blob/main/discord_interactions/__init__.py#L31" rel="noopener noreferrer"&gt;this function&lt;/a&gt; to see how it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_key_decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client_public_key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jsonify&lt;/span&gt;

    &lt;span class="c1"&gt;# https://stackoverflow.com/questions/51691730/flask-middleware-for-specific-route
&lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nd"&gt;@wraps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__decorator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# Verify request
&lt;/span&gt;            &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;X-Signature-Ed25519&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;X-Signature-Timestamp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;verify_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;signature&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;client_public_key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Bad request signature&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt;

            &lt;span class="c1"&gt;# Automatically respond to pings
&lt;/span&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;InteractionType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
                    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;InteractionResponseType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PONG&lt;/span&gt;
                &lt;span class="p"&gt;})&lt;/span&gt;

            &lt;span class="c1"&gt;# Pass through
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;f&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;__decorator&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_decorator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it fails verification, we will not be able to add the URL and it will not work. You might want to add some additional logging to the Lambda function if you are not able to add the URL successfully.&lt;/p&gt;

&lt;p&gt;This is all covered in &lt;a href="https://discord.com/developers/docs/interactions/slash-commands" rel="noopener noreferrer"&gt;the documentation for Discord Interactions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now you should be able to run the Discord slash commands. You can get the status of your ECS cluster and scale it to either 1 or 0 for &lt;code&gt;ON&lt;/code&gt; and &lt;code&gt;OFF&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;Here's an overview of what we covered:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd66t3cbd0n9vxeem6qyh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fd66t3cbd0n9vxeem6qyh.png" alt="Diagram" width="800" height="700"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;This is my computer. For development of this project (and most other projects) I used Windows with WSL2.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;GitLab CI - This is used to run our automated pipelines whenever we push a tag.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The CDK CLI is used to create, update and delete the infrastructure in our AWS account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Valheim - The client for the game server that we set up&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The public IP address of the ECS Task that can be used to connect to our server on port 2456.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The ECS Cluster that runs the actual docker container for the Valheim server. By default, the image used is &lt;a href="https://hub.docker.com/r/lloesche/valheim-server" rel="noopener noreferrer"&gt;lloesche/valheim-server&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;EFS - This is the file system that is mounted onto the container of the ECS task where our game's world data is stored.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;AWS Backup (Optional) - This is an optional feature of the &lt;code&gt;cdk-valheim&lt;/code&gt; construct that can make regular backups of our EFS file system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Events (Optional) - AWS Events can be used to scale the number of ECS tasks between 0 and 1.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This is the &lt;a href="https://github.com/gotodeploy/cdk-valheim" rel="noopener noreferrer"&gt;&lt;code&gt;cdk-valheim&lt;/code&gt;&lt;/a&gt; construct that I use in this project.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;S3 bucket for syncing data to and from EFS with DataSync (WIP)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DataSync for moving game data between EFS and S3.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Slash Commands that we set up&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Discord&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Discord Interactions sends and a &lt;code&gt;POST&lt;/code&gt; request&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The API Gateway endpoint that we configured to handle Discord Interaction &lt;code&gt;POST&lt;/code&gt; requests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Lambda function running a simple Flask app that responds to the Interaction &lt;code&gt;POST&lt;/code&gt; request.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;boto3 - This is the AWS SDK Python library included in the Python execution environment that allows us to interact with the resources in our AWS account. In particular, the interactions we use from boto3 are the &lt;code&gt;update_service&lt;/code&gt; and &lt;code&gt;describe_servics&lt;/code&gt; methods from the ECS module. This allows us to turn our server on and off and also get the status.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This represents the &lt;code&gt;valheim-server-stack&lt;/code&gt; we defined in our CDK application.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  TODO
&lt;/h2&gt;

&lt;p&gt;There are still some things that I'm working on finalizing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DataSync for easily moving data between S3 and EFS&lt;/li&gt;
&lt;li&gt;Report billing data with an additional slash command sub-command&lt;/li&gt;
&lt;li&gt;Add tagging to the resources in our stack to make the billing command easier to implement.&lt;/li&gt;
&lt;li&gt;Get feedback from the Discord, CDK and Valheim communities about what I can improve here&lt;/li&gt;
&lt;li&gt;Contribute to &lt;a href="https://github.com/gotodeploy/cdk-valheim" rel="noopener noreferrer"&gt;&lt;code&gt;gotodeploy/cdk-valheim&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks for reading!&lt;/p&gt;

</description>
      <category>cdk</category>
      <category>discord</category>
      <category>valheim</category>
      <category>python</category>
    </item>
    <item>
      <title>Session Authentication with Django, Django REST Framework and Nuxt</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Fri, 01 Jan 2021 20:01:00 +0000</pubDate>
      <link>https://dev.to/briancaffey/session-authentication-with-django-django-rest-framework-and-nuxt-ckl</link>
      <guid>https://dev.to/briancaffey/session-authentication-with-django-django-rest-framework-and-nuxt-ckl</guid>
      <description>&lt;p&gt;Originally published to &lt;a href="https://briancaffey.github.io/2021/01/01/session-authentication-with-django-django-rest-framework-and-nuxt" rel="noopener noreferrer"&gt;https://briancaffey.github.io/2021/01/01/session-authentication-with-django-django-rest-framework-and-nuxt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This will be a continuation of the discussion about how data flows in Django + Nuxt applications, looking specifically at session authentication.&lt;/p&gt;

&lt;p&gt;Here's a GitLab repo that where you can find the source code and other diagrams related to this project:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gitlab.com/briancaffey/django-nuxt-starter" rel="noopener noreferrer"&gt;https://gitlab.com/briancaffey/django-nuxt-starter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This diagram focuses on the interactions between:&lt;/p&gt;

&lt;p&gt;I. The browser&lt;/p&gt;

&lt;p&gt;II. The Nuxt server (Node process)&lt;/p&gt;

&lt;p&gt;III. The Django backend API server (gunicorn process)&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;For illustration purposes, I'm using a simple CRUD application that has two models: Users and (blog) Posts. Users can log in with email and password credentials and create, read, update and delete blog posts (CRUD). Currently I'm only doing the R (read) of CRUD: listing and viewing blog posts. Creating, updating and delete will be added later. For now, users must be logged in to see posts.&lt;/p&gt;

&lt;p&gt;I'm still learning a lot about Nuxt and how it can be used with Django and Django REST Framework. This project is an effort at documenting my learning process, learning in public and learning from mistakes, so any feedback or guidance on what I have written here would be highly appreciated!&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Nuxt?
&lt;/h2&gt;

&lt;p&gt;Using Nuxt (with Server Side Rendering, or SSR) is one of many ways to use Vue.js with Django. Vue is a progressive framework, which means that it can be gradually adopted into a project--you don't have to go all-in on the framework or rewrite the application from scratch to fit with how Vue works.&lt;/p&gt;

&lt;h3&gt;
  
  
  Different ways to use Vue with Django
&lt;/h3&gt;

&lt;p&gt;In terms of Django, here are some ways that you can use Vue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vue as a jQuery replacement for adding basic interactivity in views served by Django templates&lt;/li&gt;
&lt;li&gt;Build a static Vue application and serve it as a set of static assets in a Django project alongside routes that are served by other normal Django templates views.&lt;/li&gt;
&lt;li&gt;Build a Vue SPA which consumes a Django API (usually built with Django REST Framework or similar), and serve it over a content delivery network (CDN).&lt;/li&gt;
&lt;li&gt;Use Vue to build an Electron desktop app that uses Django as an API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these scenarios, Vue is served as either static assets (such as in the case of serving a SPA over a CDN), or Vue code is included in an HTML response from a server (where the view library, not your application, is served over a CDN), similar to how jQuery is used.&lt;/p&gt;

&lt;h3&gt;
  
  
  Different ways to use Nuxt
&lt;/h3&gt;

&lt;p&gt;Nuxt is a Framework that can be used in a few different ways, I'll briefly discus three ways in which Nuxt can be used. Common to all three of these ways of using Nuxt is the directory structure. No matter how you use Nuxt, it provides a great way to organize Vue code.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Static mode: this mode allows you to write Vue code which is built into a static HTML, and then that HTML is deployed to a CDN or webserver like NGINX. The developer (or CI/CD process) runs a command to generate HTML files for each page in the application, and these pages are served as-is when accessed by a user. I recently migrated my personal blog from Jekyll to Nuxt with full-static mode. Check it out at (&lt;a href="https://briancaffey.github.io" rel="noopener noreferrer"&gt;briancaffey.github.io&lt;/a&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SPA mode: This is similar to what you might use if you started a Vue project with Vue CLI. The project is also generated as in Static Mode, but what is generated is primarily Javascript code that is executed on the browser.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;SSR mode: Server Side Rendering is the mode that I'll be focusing on here. Unlike the other ways of using Vue that have already been discussed, this mode involves a Node.js server that will handle our requests. For example, a web request for &lt;code&gt;/posts&lt;/code&gt; is sent to our Nuxt Server (a Node.js server process) and Node.js is responsible for returning HTML that contains all of the blog Posts that we want to show (or a paginated selection of all blog posts, which is how my example blog app is built). So the Nuxt app has to make a request to our Django API server before returning fully rendered HTML page for the &lt;code&gt;/posts&lt;/code&gt; page. The user then gets the page from Nuxt, reads all of the blog posts and then decides to check out the blog posts on the second page of posts. When the user clicks on page 2, we request the second page of data from our Django API directly, not from Nuxt. The user then sees a short loading animation followed by the second page of blog posts that are loaded in using AJAX (usually with &lt;code&gt;fetch&lt;/code&gt; or axios).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Nuxt Benefits
&lt;/h3&gt;

&lt;p&gt;The main reason for using Nuxt is to render the first page loads on the server, returning a complete HTML response that can be beneficial for SEO, social sharing, and other scenarios where you need control over how a website's pages are delivered (specifically, the initial request made to the server).&lt;/p&gt;

&lt;p&gt;This type of control is not possible for applications that serve Vue over CDN since they can only request backend API data once the JS client has been requested from a CDN.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nuxt Downsides and Tradeoffs
&lt;/h3&gt;

&lt;p&gt;Using Nuxt for SSR introduces quite a bit of complexity in both the application deployment and our Vue code. The backend API won't have to change at all when moving to Nuxt from a static Vue SPA.&lt;/p&gt;

&lt;p&gt;Django alone is capable of returning fully generated, SEO-optimized HTML for each request, but applications built with Vue and Django templates may be difficult to work on as the project grows larger and larger. The Django/DRF + Nuxt approach may be more appropriate for projects with dedicated backend and frontend teams.&lt;/p&gt;

&lt;p&gt;One other potential downside is added latency because of the "double request". If the Nuxt server and the Django server are on the same machine, then this latency will probably be a non-issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagram
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fibodxhp81hysu1yun850.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fibodxhp81hysu1yun850.png" alt="Nuxt Django Auth" width="800" height="1376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This diagram looks at session authentication with a focus on the browser, the Nuxt server and the Django server. It looks at two simple user stories, ordered from top to bottom in the diagram.&lt;/p&gt;

&lt;p&gt;I. An existing application user visits the site in a new browser, navigates to the Login page, logs in with credentials and then visits a protected page: &lt;code&gt;/posts&lt;/code&gt;.&lt;br&gt;
II. The user closes the browser and then comes back directly to the &lt;code&gt;/posts&lt;/code&gt; page.&lt;/p&gt;

&lt;p&gt;These two user stories sound simple, but they touch on a lot of the features of Nuxt that make it powerful, and complicated at first (for Vue users). These include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;asyncData&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nuxtServerInit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Vuex on the client and server&lt;/li&gt;
&lt;li&gt;Custom plugin for axios&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some important parts of Nuxt that this diagram does not (yet) touch on are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Nuxt auth module (I don't know if this is relevant for my use case)&lt;/li&gt;
&lt;li&gt;Nuxt fetch property (different from the &lt;code&gt;fetch&lt;/code&gt; web API)&lt;/li&gt;
&lt;li&gt;Nuxt middleware (I'm also not sure if this would be helpful for anything I am doing in this example project)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  User story I.: Visit site, login, navigate to &lt;code&gt;/posts&lt;/code&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;User navigates to &lt;code&gt;http://domain.com/&lt;/code&gt;. This request is handled by the Nuxt server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;nuxtServerInit&lt;/code&gt; action is called (&lt;a href="https://nuxtjs.org/docs/2.x/directory-structure/store#the-nuxtserverinit-action" rel="noopener noreferrer"&gt;read more on nuxtServerInit&lt;/a&gt;). This is a special Vuex action that, if defined in &lt;code&gt;store/index.js&lt;/code&gt;, will be called once per request to the Nuxt Server (when a page is initially visited or refreshed in the browser).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;nuxtServerInit&lt;/code&gt; dispatches a Vuex action in the &lt;code&gt;user&lt;/code&gt; module called &lt;code&gt;fetchData&lt;/code&gt;. This action makes an GET request to &lt;code&gt;/api/account/&lt;/code&gt; in the Django application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;/api/account/&lt;/code&gt; gets the logged-in user's profile information.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If the request is made by an anonymous user (no user is logged in), a 403 response is returned to the Nuxt server and no account data is set in the Vuex store (on the server).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The HTML is rendered for the Home Page&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The rendered HTML generated by Vuex is sent back to the browser.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The user navigates (via a &lt;code&gt;&amp;lt;router-link&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;nuxt-link&amp;gt;&lt;/code&gt;) to the login page. Since the navigation happened through Vue router, no request to the Nuxt server is made.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;created&lt;/code&gt; hook for the Login page makes a GET request to &lt;code&gt;/api/login-set-cookie/&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;10, 11. This endpoint calls a simple view that is decorated with &lt;code&gt;@ensure_csrf_token&lt;/code&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;When the response returns to the browser, the &lt;code&gt;csrftoken&lt;/code&gt; is set in the browser.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The $apiCall function is defined in &lt;code&gt;plugins/axios.js&lt;/code&gt;, and it adds the &lt;code&gt;csrftoken&lt;/code&gt; cookie to the &lt;code&gt;X-CSRFToken&lt;/code&gt; header of API requests. This is important for POST request where the CSRF token is required. When the user fills out their email and password in the login form, the $apiCall function is called with &lt;code&gt;/api/login/&lt;/code&gt; and the email/password as credentials.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The email and password are sent as data in the POST request to &lt;code&gt;/api/login/&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;15, 16. The &lt;code&gt;/api/login/&lt;/code&gt; URL calls the &lt;code&gt;login_view&lt;/code&gt; which makes use of two functions from &lt;code&gt;django.contrib.auth&lt;/code&gt;: &lt;code&gt;authenticate&lt;/code&gt; and &lt;code&gt;login&lt;/code&gt;. &lt;code&gt;authenticate&lt;/code&gt; gets a user from the provided email/password, and the &lt;code&gt;login&lt;/code&gt; function sets an HttpOnly &lt;code&gt;sessionid&lt;/code&gt; session cookie on the response.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The HttpOnly &lt;code&gt;sessionid&lt;/code&gt; cookie is automatically set on the browser when the &lt;code&gt;/api/login/&lt;/code&gt; request returns successfully.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When this &lt;code&gt;/api/login/&lt;/code&gt; request returns successfully, a value in the &lt;code&gt;auth&lt;/code&gt; Vuex module is set to keep track of the current user's logged in state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Next, a GET request is made to &lt;code&gt;/api/account/&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;20, 21. Since the &lt;code&gt;sessionid&lt;/code&gt; cookie is set and sent along with the request automatically, this request will succeed.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;When the &lt;code&gt;/api/account/&lt;/code&gt; request returns, the user's account information is saved to the &lt;code&gt;user&lt;/code&gt; Vuex module. At this point, the client may redirect automatically to the home page, or user account page, dashboard, etc.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Now logged in, the user navigates (again via Vue router) to &lt;code&gt;/posts&lt;/code&gt;, a page that shows a paginated view of all blog posts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This page has an &lt;code&gt;asyncData&lt;/code&gt; method which is called when the page component is created and it dispatches a Vuex action &lt;code&gt;posts/fetchData&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This Vuex action makes a GET request to &lt;code&gt;/api/posts/&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;26, 27. &lt;code&gt;/api/posts/&lt;/code&gt; uses a &lt;code&gt;ModelViewSet&lt;/code&gt; and returns a paginated list of blog posts&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When the &lt;code&gt;/api/posts/&lt;/code&gt; request returns successfully, the blog post data is saved to the &lt;code&gt;blog&lt;/code&gt; Vuex module.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  User story II.: Logged in user opens new browser window and revisits &lt;code&gt;/posts&lt;/code&gt;
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The user closes their browser and then opens a new browser window and navigates to &lt;code&gt;/posts&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;nuxtServerInit&lt;/code&gt; is called as usual,&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;user/fetchData&lt;/code&gt; action is called. This action makes a GET request to &lt;code&gt;/api/account/&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;/api/account/&lt;/code&gt; request returns successfully. The &lt;code&gt;sessionid&lt;/code&gt; cookie is passed along from the browser to the API request that is made from the Nuxt server to the backend API (&lt;code&gt;/api/account/&lt;/code&gt;).  User account data is then set on the Vuex &lt;code&gt;user&lt;/code&gt; module.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;asyncData&lt;/code&gt; method for the &lt;code&gt;/posts&lt;/code&gt; pages is called.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;asyncData&lt;/code&gt; dispatches a Vuex action &lt;code&gt;posts/fetchData&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;posts/fetchData&lt;/code&gt; makes an API request to &lt;code&gt;/api/posts/&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;/api/posts/&lt;/code&gt; request is handled by a &lt;code&gt;ModelViewSet&lt;/code&gt; for the &lt;code&gt;Post&lt;/code&gt; model that gets blog posts and then sets them to the Vuex store (on the server) when the request returns a response (to the Nuxt server).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Once the async data fetching is compete (&lt;code&gt;nuxtServerInit&lt;/code&gt; and &lt;code&gt;asyncData&lt;/code&gt; for the &lt;code&gt;/posts&lt;/code&gt; page), the page HTML is rendered using the Vuex store data stored on the server. The Vuex data is sent back with the rendered HTML (I think this is how it works).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Finally, the user sees the list of blog posts. The page is loaded "at once"; there is no waiting for data to load after loading the page initially.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Complexity
&lt;/h3&gt;

&lt;p&gt;Is this authentication process overly complicated? When I make these diagrams, I try to make simple concept as detailed as possible, but there are a lot of distinct actions being taken in many different parts of the application and getting them all into one diagram was tricky.&lt;/p&gt;

&lt;h3&gt;
  
  
  HttpOnly Session Cookies
&lt;/h3&gt;

&lt;p&gt;Session authentication is the officially recommended way to do authentication with Django REST Framework for clients that run in the browser. However, there seem to be lots of people using JWT with DRF and Javascript clients that run in the browser. The main argument against doing this is that the JWT must be stored in a Javascript-accessible store (localStorage or Cookies) so it can be passed with each request. Many people are also interested in trying to store JWT for authentication in HttpOnly cookies to harden client-side security. I'm very curious to know if anyone is actually doing this, and what the implementation looks like. While &lt;code&gt;djangorestframework_simplejwt&lt;/code&gt; doesn't support HttpOnly, there seems to be lots of interest in doing this. I think it might be possible with a special middleware, so let me know if anyone is interested in proof-of-concept/diagram for that.&lt;/p&gt;

&lt;p&gt;Some use cases for JWT and other token authentication methods with DRF might include native mobile apps or Desktop apps. For most cases, I think session authentication with Django's built in session cookies for DRF authentication is the best option. JWTs also have no clear solution for logging out, which may be important for some security considerations. The concept of stateless authentication is interesting, but for most use cases I would argue that it is not worth doing. Let me know if anyone has thoughts on this, I'm curious to see what everyone thinks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Next Steps
&lt;/h3&gt;

&lt;p&gt;My next steps for this project/repo are to deploy this to a production environment as soon as I have time to do so. My local setup has been working well, and I think it should work well for a simple DigitalOcean docker swarm deployment like I have done with other Django + Vue projects.&lt;/p&gt;

&lt;p&gt;I also want to add the create, update and delete functionality for posts, improve error handling with API calls, add form validation, and maybe write some tests with Jest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;p&gt;Here are some questions and areas that I still need to investigate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nuxt Composition API
&lt;/h3&gt;

&lt;p&gt;I have seen that there is a Composition API module for Nuxt. I have only just now started looking at Composition API examples and documentation for "vanilla" Vue, but I have heard that the Nuxt Composition API module has some additional features specifically for use with Nuxt, so I'm curious to learn what these are.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nuxt v3 and Vue 3
&lt;/h3&gt;

&lt;p&gt;Nuxt looks like it has plans to support Vue 3, so I am interested to learn more about Vue 3 as it is adopted by Vue frameworks such as Nuxt and Quasar.&lt;/p&gt;

&lt;h3&gt;
  
  
  Nuxt's fetch method, server middleware, Nuxt auth module
&lt;/h3&gt;

</description>
      <category>django</category>
      <category>nuxt</category>
      <category>authentication</category>
    </item>
    <item>
      <title>Building web applications with Django, Django REST Framework and Nuxt</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Mon, 28 Dec 2020 04:26:28 +0000</pubDate>
      <link>https://dev.to/briancaffey/building-web-applications-with-django-django-rest-1k28</link>
      <guid>https://dev.to/briancaffey/building-web-applications-with-django-django-rest-1k28</guid>
      <description>&lt;p&gt;Over the holidays between lots of big meals and many naps, I tried to tackle one more goal of mine before this year come to an end: building an application with Django and Nuxt.js.&lt;/p&gt;

&lt;p&gt;This year I rebuilt my personal blog (&lt;a href="https://briancaffey.github.io/" rel="noopener noreferrer"&gt;briancaffey.github.io&lt;/a&gt;) with Nuxt.js, the &lt;code&gt;@nuxt/content&lt;/code&gt; headless git-based CMS and TailwindCSS. It is statically generated with Nuxt's full-static mode and has been really enjoyable to work with. I have also learned a lot more about SEO and how Nuxt helps improve Vue applications' SEO. I have also been working a lot with Django and Vue.js applications where Django serves as an API to a Vue.js SPA. This combination of technologies works well for a lot of use cases, but it falls short in SEO. Nuxt also provides a great way to organize large Vue.js projects which I have been finding very helpful. For these reasons, combining Django and Nuxt has been something that I have wanted to try for a while, so this article will share some of my experiences in recent efforts to build with these two frameworks. I took &lt;a href="https://gitlab.com/briancaffey/django-nuxt-starter/-/blob/develop/STEP_BY_STEP.md" rel="noopener noreferrer"&gt;detailed notes of each step of the project setup&lt;/a&gt; starting from an empty repository, and I put together a diagram of my understanding of how data flows in the application.&lt;/p&gt;

&lt;p&gt;Here's the link to the project repository that I'll be referencing: &lt;a href="https://gitlab.com/briancaffey/django-nuxt-starter" rel="noopener noreferrer"&gt;https://gitlab.com/briancaffey/django-nuxt-starter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This article will focus on explaining the project through the diagram shown below. I added two types of labels: letters and numbers. The letters will introduce each component of the application and its role in the application as a whole. The numbers summarize how data flows through the different components in my sample blog application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfsvvuhmxuw9k5wo05d1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyfsvvuhmxuw9k5wo05d1.png" alt="Diagram" width="800" height="999"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagram components
&lt;/h2&gt;

&lt;p&gt;A. Your computer - Possibly also your development machine which is running the application in docker containers with docker-compose.&lt;/p&gt;

&lt;p&gt;B. NGINX - This is the "front desk" of the application that does a few different things. It is the first component that web requests come to. It serves as a reverse proxy which does path-based routing. It looks at the URL request and determines where to send it. For example: &lt;code&gt;/api/posts/1&lt;/code&gt;, &lt;code&gt;/dashboard/&lt;/code&gt;, &lt;code&gt;/admin/&lt;/code&gt; could all be routed differently depending on the NGINX configuration file. We will look at this again in the next section. This  component, like most of the other things in the diagram, runs in a container. NGINX can also serve static files for our Django app and do TLS termination to make our application available over a secure HTTPS connection.&lt;/p&gt;

&lt;p&gt;C. Nuxt.JS server - The first "S" in SSR (server side rendering). It is a Node.js process that renders HTML from Vue components that we define in our Nuxt app, as well as data fetched from other servers/APIs before returning HTML back to the client.&lt;/p&gt;

&lt;p&gt;D. Django server - This runs the WSGI application with a gunicorn process in a container.&lt;/p&gt;

&lt;p&gt;E. Django REST Framework is a Django package the facilitates the creation of REST API endpoints. This is part of the Django application, it primarily takes care of data serialization (which can be thought of as translating between JSON and Python objects that represent rows of data in our Postgres database)&lt;/p&gt;

&lt;p&gt;F. This is the Postgres database, also a containerized service. It is on the same docker network as the Django/gunicorn application, so the Django application can connect to the Postgres database using the hostname &lt;code&gt;postgres&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;G. docker-compose is used to orchestrate the docker network, containers and volumes that make up the application.&lt;/p&gt;

&lt;p&gt;H. This box represents the docker network that allows for easy networking between services. We will come back to this the significance of this in the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data flow in the application
&lt;/h2&gt;

&lt;p&gt;The simple application I have built for this demonstration is a blog. There is only a list view and a detail view for simple blog post model with three fields: title, body and created date. For the list view, the frontend (Nuxt) route is &lt;code&gt;/posts/&lt;/code&gt; and the backend route is &lt;code&gt;/api/posts/&lt;/code&gt; for the detail view the frontend route is &lt;code&gt;/posts/_id&lt;/code&gt; and the API route is &lt;code&gt;/api/posts/_id/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The data flow shown here will walk through what happens when a user visits &lt;code&gt;http://localhost/posts/&lt;/code&gt;, and then show what happens when the user clicks on one of the listed posts to see the detail view of the post (&lt;code&gt;http://localhost/posts/2&lt;/code&gt;).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;docker-compose up&lt;/code&gt; is one command that is used to start the entire application in local development. This exposes the NGINX process on port 80 of the host machine (your laptop).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When the application is running on your machine and you navigate to &lt;code&gt;http://localhost/posts/&lt;/code&gt;, the request is first handled by NGINX.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As we mentioned earlier, NGINX's path-based routing sends all requests that do not start with &lt;code&gt;/api/*&lt;/code&gt; or &lt;code&gt;/admin/*&lt;/code&gt; to the Nuxt.js server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When the request gets to the Nuxt server, the Nuxt lifecycle methods start. The important one that I'm using so far is &lt;code&gt;asyncData&lt;/code&gt;. This property is used to request data that will be used in the rendering of our HTML response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Inside of &lt;code&gt;asyncData&lt;/code&gt;, the application uses axios to make a request to &lt;code&gt;/api/posts/&lt;/code&gt; (for example). In &lt;code&gt;nuxt.config.js&lt;/code&gt;, the &lt;code&gt;privateRuntimeConfig&lt;/code&gt; sets a baseUrl value for axios to &lt;code&gt;http://backend:8000&lt;/code&gt;. Since the Nuxt server is on the same docker network as the backend Django/gunicorn server, the Nuxt server is able to resolve &lt;code&gt;http://backend&lt;/code&gt; to the address of the backend server.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Django processes this endpoint, using the &lt;code&gt;PostViewSet&lt;/code&gt;, the views of which have been added to &lt;code&gt;urlpatterns&lt;/code&gt; in &lt;code&gt;blog/urls.py&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The &lt;code&gt;PostViewSet&lt;/code&gt; makes a database query on the &lt;code&gt;posts&lt;/code&gt; which is used to serialize the data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Django server returns the response to the original axios call.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The data returned from Django is used to render the HTML response.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The HTML response from the Nuxt server is sent back to the browser that originally navigated to &lt;code&gt;http://localhost/posts/&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The user is presented with page that lists blog posts. Each blog posts lists to a detail view. When a blog post (let's say the post with &lt;code&gt;id&lt;/code&gt; of 2) is clicked on, a request for &lt;code&gt;/posts/2/&lt;/code&gt; is made directly to the Django backend. The &lt;code&gt;browserBaseURL&lt;/code&gt; value in the &lt;code&gt;axios&lt;/code&gt; settings under &lt;code&gt;publicRuntimeConfig&lt;/code&gt; defined in &lt;code&gt;nuxt.config.js&lt;/code&gt; is set to &lt;code&gt;http://localhost&lt;/code&gt;, so the request is made to &lt;code&gt;http://localhost/api/posts/2/&lt;/code&gt;. To clarify, since we are making this request using axios in the browser, we can't make a request to &lt;code&gt;http://backend:8000/api/posts/2/&lt;/code&gt; like we did in step 4 (&lt;code&gt;http://backend:8000/api/posts/&lt;/code&gt;) because the browser doesn't know how to resolve the &lt;code&gt;backend&lt;/code&gt; hostname.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This request to &lt;code&gt;http://localhost/api/posts/2/&lt;/code&gt;, like all others, first goes to NGINX which sends it to the backend since the path starts with &lt;code&gt;/api/&lt;/code&gt;. At this point the application functions like a regular Vue SPA making axios calls to a backend service. This is because we used &lt;code&gt;&amp;lt;nuxt-link&amp;gt;&lt;/code&gt; for the posts listed in the posts list view. If we used &lt;code&gt;&amp;lt;a&amp;gt;&lt;/code&gt; tags, we would go through the same process as in step 4 where the HTML is rendered on the Nuxt server and sent back to the browser all at once.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Discussion
&lt;/h2&gt;

&lt;p&gt;My main takeaway is that using Nuxt and Django together can give you good SEO and a great SPA experience at the same time. Using Django alone, or Django with traditional non SSR Vue makes this harder to do. Being a progressive framework, there are a lot of ways to use Vue with any other backend. From what I have heard, most people use Vue via CDN similar to how jQuery was and still is delivered for use in the browser.&lt;/p&gt;

&lt;p&gt;There is additional work in setting up 3 servers for a single application (Nuxt, Django and NGINX), but the tradeoff is that I am (at least I feel) very productive writing frontend logic in Vue and backend logic with DRF. I have never liked working with Django templates and I used to know a lot more about them than I do now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Spotlight for baserow.io's awesome open-source Django Nuxt application
&lt;/h2&gt;

&lt;p&gt;Lastly I want to mention that there are some great resources in the &lt;a href="https://github.com/nuxt-community/awesome-nuxt" rel="noopener noreferrer"&gt;nuxt-community/awesome-nuxt&lt;/a&gt; GitHub repo. There's one project that really stood out to me when I searched for "django" projects in the README, and that project is called &lt;a href="https://baserow.io/" rel="noopener noreferrer"&gt;baserow.io&lt;/a&gt; (repo: &lt;a href="https://gitlab.com/bramw/baserow" rel="noopener noreferrer"&gt;https://gitlab.com/bramw/baserow&lt;/a&gt;). Please check this repo our if you are interested in Django and Nuxt. This company is building an open source no-code database, similar to Airtable which I have worked with before.&lt;/p&gt;

&lt;p&gt;Their entire product is open source and I have been very impressed with what I have seen. Please go give that project a star or consider becoming a Github sponsor if you are interested. I'm not affiliated with that project in any way, but I'll be referencing how they use Django and Nuxt to build their application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;There is a still a lot I have to learn about Nuxt. I'm still very new to the Framework and this is my first time using Nuxt's SSR mode. Nuxt seems to have its own way of doing lots of things that I'm used to doing in Vue. There is a very supportive community and well-maintained official packages to help with lots of things, like the &lt;code&gt;@nuxt/axios&lt;/code&gt; package that I'm using.&lt;/p&gt;

&lt;p&gt;My next step is to keep expanding my blog application. One thing I didn't mention is authentication. I plan on using Django session authentication for authenticating request to Django. It seems that it already works correctly in my application (logging in through Django admin and then navigating to Nuxt routes that make Django requests are working only when I'm logged in.) I think I have an idea about how Vuex, authentication and route guards will work together, but I haven't gotten there yet. If anyone has some good reference projects or recommendations on how to expand on what I already have, please let me know!&lt;/p&gt;

&lt;p&gt;I know that Nuxt has an auth module, so I need to see if that is relevant for what I want need in my application. I also need to continue reading the Nuxt documentation. I still don't know what I don't know about Nuxt and the plugins and modules that it makes available. I also noticed that Nuxt has it's own version of the Vue 3 Composition API, something I am just now starting to learn more about, so that it another area I'll need to dig into eventually.&lt;/p&gt;

</description>
      <category>django</category>
      <category>nuxt</category>
      <category>drf</category>
      <category>vue</category>
    </item>
    <item>
      <title>Weekend project update: Open SEC Data</title>
      <dc:creator>Brian Caffey</dc:creator>
      <pubDate>Sun, 29 Nov 2020 23:39:59 +0000</pubDate>
      <link>https://dev.to/briancaffey/weekend-project-update-open-sec-data-1g77</link>
      <guid>https://dev.to/briancaffey/weekend-project-update-open-sec-data-1g77</guid>
      <description>&lt;p&gt;Here's an early look at a project I have been working on to practice some Django and Vue.js concepts: Open SEC Data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://opensecdata.ga" rel="noopener noreferrer"&gt;https://opensecdata.ga&lt;/a&gt; (project staging website, deployed to docker swarm cluster running on DigitalOcean)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://gitlab.com/briancaffey/sec-filings-app" rel="noopener noreferrer"&gt;https://gitlab.com/briancaffey/sec-filings-app&lt;/a&gt; (main repository, requires GitLab account)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/briancaffey/sec-filings-app" rel="noopener noreferrer"&gt;https://github.com/briancaffey/sec-filings-app&lt;/a&gt; (mirror, no account required to view)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This project uses Django, DRF and Celery to read public SEC filings from &lt;a href="https://www.sec.gov/Archives/edgar/full-index/" rel="noopener noreferrer"&gt;sec.gov&lt;/a&gt;, build it into an API which is consumed through a Vue.js application. I'm currently focused on 13F filings which are required for large US investment funds managing over $100 million USD. There is data dating back to 1993 and it is published quarterly. &lt;/p&gt;

&lt;p&gt;Here are some of the things I'm focusing on in this project in no particular order: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Getting better at Django REST Framework. This project has been helping me apply some of the parts of DRF that I have found difficult. I'm currently using ViewSets which feels function-based views inside of class-based views. They are flexible, but I would like to add more abstraction with filtering&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Django admin. While this project primarily uses Django as a REST API with Django REST Framework, I have tried to take advantage of the Django admin to build out helpful views that can be used to spot check the data I'm creating. Most of my API is read-only, this makes things pretty simple. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Moderately complex paginated data tables with Vue. I work with lots of paginated table data, and I think there is a better way to do abstract some of the repeated logic that I use (getting and setting current page, rows per page). I'm using Vuex, and I have heard of module factories, but I'm thinking that there will be a better way to do this when Vue 3 officially comes to Quasar Framework (Quasar is a Vue.js framework).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Session authentication with DRF. There are a lot of guides showing how to use JWT and Token Authentication for DRF with Javascript frontends. The DRF recommends using Session Authentication for such use cases as a web-base Javascript client, so I hope I can promote some best practices around how to use Django's built-in session authentication for use with the Django REST Framework using an HttpOnly session cookie. I also understand that all security decisions have trade-offs, and I'm trying to understand what trade-offs come with handling authentication in this way. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Social authentication. I have previously setup social authentication with Google, Facebook and GitHub using Python Social Auth. I think it is a great package, and it adds a lot of flexibility with it's concept of pipelines, but I haven't done much with these yet, so I'm hoping to dig in further and better understand how I can make better use of social authentication in my app. This app uses Linkedin 0Auth2 with a custom user model. Logging in with Linkedin account gives you the ability to request an API Token (Django REST Framework's Token) to access the public API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automatic API documentation with OpenAPI. Swagger/OpenAPI seems like nice way to document and API, so I'm hoping to build best practices around how to document a DRF API automatically with OpenAPI and Swagger UI. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CI/CD with GitLab and docker swarm. I will admit that I am huge GitLab fan. I love how flexible their CI/CD pipelines are. Being a docker fan as well, I chose to use docker swarm for this project to keep things simple and straightforward. I think one under-appreciate feature of docker is being able to set &lt;code&gt;DOCKER_HOST&lt;/code&gt; to an SSH connection, such as &lt;code&gt;ssh://root@123.456.789.10&lt;/code&gt;. This let's you control the remote docker host without needing to SSH to it first, and it is also how I'm able to deploy and run management commands "manually" through the GitLab UI.  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Productive development environment. To start the project, you only need to run docker-compose up (after copying &lt;code&gt;.env.template&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt; in the root directory for storing sensitive data outside of git such as LinkedIn OAuth2 keys). The development environment is very similar to how this project runs in production with some additional utilities for monitoring and debugging such as pgadmin4, flower (for celery), redis commander (a GUI for viewing redis databases), Django debug toolbar (a must have for any Django project, I believe), runserver_plus with Werkzeug, and others. Also, the backend and frontend hot reload automatically with the help of webpack for Vue and watchdog for Django and Celery. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automatic TLS certificate generation with Traefik. For a simple project in docker swarm, I'm really happy with how simple it is to request TLS certificates from Let's Encrypt automatically with Traefik. There are no scripts, cron jobs or one-time setup jobs, it just seems to work out of the box if configured correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Testing with pytest. I have only been trying to test most of my API views so far. I really like using factory with pytest, so I use that in most of my tests.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's all I have for now. I have a long list of questions, things I want to improve, add and experiment with, here are just a few that come to mind: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Frontend testing. I don't have any component testing or e2d tests, so this would be good to add eventually. Since I'm using a component library and my app uses these components directly, I'm not exactly sure how much testing I should be doing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Data verification/validation. There are a lot of site that do provide similar data, WhaleWisdom is the biggest one that I know of. Once I get more data built on the site it would be good to spot check some of the values. There are some nuances to the filing data that I haven't addressed, such as Amendment filings and additions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Calculating period changes. One of the features that I'm not sure how best to implement is the ability to sort holdings for a filer in a given period on the percent increase from the last period. One way would be to add these as additional fields to the Holding model and then calculate these values as I process the data in celery. If I process the data from recent periods to later periods, I will have to update these values once the previous period has been processed, so it would be an additional check to do. I'll probably post this question here in more detail later. Here's &lt;a href="https://whalewisdom.com/filer/ubs-ag#tabholdings_tab_link" rel="noopener noreferrer"&gt;an example of what this means from WhaleWisdom&lt;/a&gt;. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Accessing LinkedIn profile data to populate fields on my CustomUser model.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Scaling? I have a lot more experience with deploying projects to AWS which is built around the ability to scale. I don't know a project on DigitalOcean would be scaled automatically. A single node docker swarm cluster while take some time to process all of the data. I would probably be better of scaling vertically with much bigger droplets and higher celery concurrency.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docker swarm secrets. I'm currently using environment variables to pass secrets stored in GitLab CI when I build images and deploy to docker swarm. I would like to learn how to properly use swarm secrets and work them into my CI/CD pipeline. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;As I mentioned above, I'm also interested in updating this project to Vue3 and to apply some of its new features to this project. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Use pipenv, poetry or some other way of pinning secondary python dependencies. Does anyone have a recommendation on how best to do this with docker. I have always thought that docker &lt;em&gt;is&lt;/em&gt; the virtual environment, but I realize that some versions of indirect dependencies may change when pip installing without using a lockfile similar to &lt;code&gt;package-lock.json&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>django</category>
      <category>gitlab</category>
      <category>vue</category>
      <category>data</category>
    </item>
  </channel>
</rss>
