<?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>Forem: Caleb Lemoine</title>
    <description>The latest articles on Forem by Caleb Lemoine (@circa10a).</description>
    <link>https://forem.com/circa10a</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%2F315518%2Fe65ee8dd-d240-444e-bdfb-04290ba4a8b0.jpg</url>
      <title>Forem: Caleb Lemoine</title>
      <link>https://forem.com/circa10a</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/circa10a"/>
    <language>en</language>
    <item>
      <title>How I use "AI" to entertain my cat</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Fri, 03 Nov 2023 17:52:14 +0000</pubDate>
      <link>https://forem.com/circa10a/how-i-use-image-classification-to-entertain-my-cat-3g1l</link>
      <guid>https://forem.com/circa10a/how-i-use-image-classification-to-entertain-my-cat-3g1l</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;It's been a while since I've made a dev.to post and I wanted to share my latest "dumb" project. My cat, Max, really enjoys watching wildlife in my backyard through the back door. There's birds, squirrels, opossums, and even a family of tree climbing rats. It's his favorite thing to do, but there's one "problem". The wildlife isn't always present and he gets pretty pissed about it. He often comes into my home office yelling at me to summon birds, but little does he know, I'm no Disney princess. What's frustrating for me is that even when wildlife is present, he'll often miss them because he's off doing other cat things. I thought, if only I could automate letting him know that he's missing some good bird watchin'!&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;I had the idea of pointing an IP camera out the back door and figure out how to detect animals then train him to go peek when a familiar sound is played. This was more difficult than I thought. After hours of reading, googling, browsing GitHub, there was no open source solution available to perform this simple idea out of the box. Welp, guess I'll build it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hardware
&lt;/h3&gt;

&lt;p&gt;After doing some research on IP cameras that support an easy access video stream via RTSP, I got a cheap &lt;a href="https://www.amazon.com/Tapo-security-indoor-pet-camera/dp/B0866S3D82" rel="noopener noreferrer"&gt;TP Link Tapo C100 camera&lt;/a&gt; for $20. I set it up on my home network using the app and placed it at my back door for a similar viewing angle of Max's.&lt;/p&gt;

&lt;p&gt;Here's the setup, not super elegant, but it'll do.&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%2F9pa2jpb1gbnnjqghrrv2.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%2F9pa2jpb1gbnnjqghrrv2.jpg" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And then here's the view from the camera itself.&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%2Fbmv3kv0n8a2agd1vr6lc.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%2Fbmv3kv0n8a2agd1vr6lc.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Perfect view of the back patio to see all the animals.&lt;/p&gt;

&lt;h3&gt;
  
  
  Software
&lt;/h3&gt;

&lt;p&gt;Next, I needed to figure out, how can I access the stream, recognize an animal, then let Max know? There are tons of examples of recognizing an object via camera frames, but I ultimately found this python library called &lt;a href="https://github.com/ultralytics/ultralytics" rel="noopener noreferrer"&gt;ultralytics&lt;/a&gt; that supports RTSP streams and classifying objects in the video frames using pre-built models. The &lt;a href="https://docs.ultralytics.com/modes/predict/?h=rtsp#inference-sources" rel="noopener noreferrer"&gt;docs&lt;/a&gt; looked like it would be pretty low effort, so after some experimentation, I was successful in having the ultralytics library recognize objects from my cheap camera!&lt;/p&gt;

&lt;p&gt;The gist that was needed to get it working:&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;ultralytics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;YOLO&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;YOLO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;yolov8s.pt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&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="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rtsp://&amp;lt;username&amp;gt;:&amp;lt;password&amp;gt;@192.168.1.101/stream1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&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;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;box&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="n"&gt;boxes&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="n"&gt;class_id&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;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;detected_object_name&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;names&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;class_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;detected_object_confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Object detected: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;detected_object_name&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="nf"&gt;print&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;confidence: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;detected_object_confidence&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OK, now I needed to recognize animals. Since the underlying model used by ultralytics is based on the widely popular COCO model, (Common Objects in Context), I found a &lt;a href="https://gist.github.com/AruniRC/7b3dadd004da04c80198557db5da4bda" rel="noopener noreferrer"&gt;list&lt;/a&gt; of the objects the model could detect. In that list, there were a few things that stuck out to me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bird (of course)&lt;/li&gt;
&lt;li&gt;cat&lt;/li&gt;
&lt;li&gt;dog &lt;/li&gt;
&lt;li&gt;mouse&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After some testing of the previously mentioned code, I noticed that squirrels were often detected as dogs or cats based on the size of them. So I modified the code a bit to filter based on those objects supported by the model like so:&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;ultralytics&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;YOLO&lt;/span&gt;

&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;YOLO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;yolov8s.pt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;results&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="nf"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rtsp://&amp;lt;username&amp;gt;:&amp;lt;password&amp;gt;@192.168.1.101/stream1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;verbose&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;bird&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;dog&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;cat&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;mouse&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;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;boxes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;box&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="n"&gt;boxes&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="n"&gt;class_id&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;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;detected_object_name&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;names&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;class_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;detected_object_confidence&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;box&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&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;detected_object_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;filters&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Object detected: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;detected_object_name&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="nf"&gt;print&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;confidence: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;detected_object_confidence&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And boom! We recognize animals!&lt;/p&gt;

&lt;p&gt;Next, I needed to wire this up to my home speakers and play a sound familiar to Max. In the before times of not watching live animals outside, Max liked it when I'd play some &lt;a href="https://www.youtube.com/watch?v=S9hDCskzWSM" rel="noopener noreferrer"&gt;bird videos on YouTube&lt;/a&gt; for him and they would all start with the same "chirp" sound. He knew this sound meant bird watching time. So I downloaded the video, extracted the audio, then split the chirp out into a custom 4 second &lt;code&gt;.mp3&lt;/code&gt; and stored it on my local &lt;a href="https://www.home-assistant.io/" rel="noopener noreferrer"&gt;Home Assistant&lt;/a&gt; instance which was already integrated with my &lt;a href="https://www.amazon.com/dp/B0CGYFYY34/ref=twister_B0CGYXJ4Z7?_encoding=UTF8&amp;amp;psc=1" rel="noopener noreferrer"&gt;Google Nest speakers&lt;/a&gt;. Luckily, Home Assistant's API is pretty friendly, but the docs definitely suck. Once I added the &lt;code&gt;.mp3&lt;/code&gt; file onto my Raspberry Pi where Home Assistant is hosted, I was able to trigger the sound to play on my speakers with this simple request to its REST API:&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;requests&lt;/span&gt;

&lt;span class="n"&gt;request_opts&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;url&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;http://192.168.1.100:8123/api/services/media_player/play_media&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;method&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;POST&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;headers&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;Content-Type&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;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;Authorization&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;Bearer &amp;lt;Home Assistant API token&amp;gt;&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;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;entity_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="s"&gt;&amp;lt;Nest Speaker ID&amp;gt;&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;media_content_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="s"&gt;/local/chirp.mp3&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;media_content_type&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;audio/mp3&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;timeout&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&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;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;request_opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Raise an error in the event of a non 2XX response code
&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Open source all the things
&lt;/h3&gt;

&lt;p&gt;Then it dawned on me. This should be a configurable open source program that can be configured to do a thing, when a camera sees a thing, but also let a user pick how frequently they want to be annoyed with bird chirp sounds in their house. I wanted to make the program very configurable because there's no way I was going to write all of this again for my next computer vision related project. So I created &lt;a href="https://github.com/circa10a/cv-notifier" rel="noopener noreferrer"&gt;cv-notifier&lt;/a&gt;. A program that's super configurable and allows anyone to replicate what I've done so far.&lt;/p&gt;

&lt;p&gt;TLDR, input a config to &lt;code&gt;cv-notifier&lt;/code&gt; and it'll handle the rest:&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;source&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rtsp://$STREAM_USER:$STREAM_PASSWORD@192.168.1.101/stream1'&lt;/span&gt;
  &lt;span class="na"&gt;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;startTime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;07:00'&lt;/span&gt;
    &lt;span class="na"&gt;endTime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;18:00'&lt;/span&gt;
  &lt;span class="na"&gt;webhooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:8080/someDumbFutureAPI&lt;/span&gt;
      &lt;span class="na"&gt;notifyInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;900&lt;/span&gt;
      &lt;span class="na"&gt;objects&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;bird&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;cat&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;dog&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mouse&lt;/span&gt;
      &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;POST'&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
        &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bearer $API_TOKEN&lt;/span&gt;
      &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="s"&gt;{&lt;/span&gt;
          &lt;span class="s"&gt;"someKey": "$object_name detected with confidence score of $object_confidence"&lt;/span&gt;
        &lt;span class="s"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;p&gt;Lots of techy jargon up until this point, but the question remains... what did Max think? Well at first it confused him. The sound that he previously knew which only came from the TV was now playing in the kitchen and he didn't know how to respond. So when the chirp would play, I'd pick him up, take him to the back door, and show him that it meant animals. After doing that 4-5 times, he started to get it. Then when the chirp sound would play, he'd scurry across the house to go see what animals were out there!&lt;/p&gt;

&lt;p&gt;The app has been running every day for a few months now. There lies another question, does it still work for Max? Eh, not really. He's gotten a bit numb to it since he's learned it goes off frequently enough that he can go look at birds pretty much whenever he pleases. He'll still go look occasionally, but he's not excited about it as he once was. Nonetheless, it was a fun project, I learned some cool things about object detection, and now I have a product I can use when I want to detect more objects in the future and call some random API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fin
&lt;/h2&gt;

&lt;p&gt;By this point, you may be thinking to yourself, "What does Max look like?". Well here's a picture of the little guy along with a video of Max bird watching!&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Max&lt;/th&gt;
&lt;th&gt;More Max&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2Fdvo77shad6pw907v1hpv.jpg" alt=" " width="632" height="1330"&gt;&lt;/td&gt;
&lt;td&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%2Fs248yly812s2l4rnxgy7.gif" alt=" " width="80" height="141"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

</description>
      <category>opensource</category>
      <category>showdev</category>
      <category>ai</category>
    </item>
    <item>
      <title>How to share a volume between cloud servers using DigitalOcean Spaces</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Sat, 22 Jan 2022 19:51:33 +0000</pubDate>
      <link>https://forem.com/circa10a/how-to-share-a-volume-between-instances-using-digitalocean-spaces-2ea</link>
      <guid>https://forem.com/circa10a/how-to-share-a-volume-between-instances-using-digitalocean-spaces-2ea</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;So you need to share files across multiple servers in the cloud, but then you come to find out you can only attach a volume to one host! What do you do?!&lt;/p&gt;

&lt;p&gt;Well, you have a few options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-an-nfs-mount-on-ubuntu-20-04" rel="noopener noreferrer"&gt;Create an NFS mount using another server&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You &lt;em&gt;could&lt;/em&gt; create a Network Filesystem using another server, but this introduces a few challenges:&lt;/li&gt;
&lt;li&gt; Your storage capacity is bound to &lt;strong&gt;one&lt;/strong&gt; underlying server, creating a single point of failure.&lt;/li&gt;
&lt;li&gt;You need some decent linux chops to get this working and automate it.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You can create a filesystem mount using &lt;a href="https://github.com/s3fs-fuse/s3fs-fuse" rel="noopener noreferrer"&gt;s3fs&lt;/a&gt; and &lt;a href="https://www.digitalocean.com/products/spaces/" rel="noopener noreferrer"&gt;DigitalOcean Spaces&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Since DigitalOcean Spaces uses Amazon's S3 protocol, we don't actually have to use AWS S3 to use s3fs, we just need storage that implements the protocol.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why
&lt;/h2&gt;

&lt;p&gt;Why would you use object storage for this filesystem to share between servers?&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It's cloud native.&lt;/li&gt;
&lt;li&gt;It's highly available.&lt;/li&gt;
&lt;li&gt;It's performant.&lt;/li&gt;
&lt;li&gt;It's cheap. $5/month for 250G!&lt;/li&gt;
&lt;li&gt;You don't have to maintain and secure a separate server for storage.&lt;/li&gt;
&lt;li&gt;You can use the object storage for other applications outside of your servers via HTTP.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to
&lt;/h2&gt;

&lt;p&gt;OK, so this sounds pretty great right? You can have some amazing object storage power your storage needs to easily share files across your servers. Today, I'll show you how using &lt;a href="https://www.terraform.io/" rel="noopener noreferrer"&gt;Terraform&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create a DigitalOcean Spaces access key
&lt;/h3&gt;

&lt;p&gt;First you'll want to login to the &lt;a href="https://www.digitalocean.com/" rel="noopener noreferrer"&gt;DigitalOcean console&lt;/a&gt; and create a new access key + access key secret. This will be used to authenticate with your DigitalOcean Spaces bucket to ensure only you can access the storage.&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%2Fsapdexs5zxsijz2e3dgb.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%2Fsapdexs5zxsijz2e3dgb.png" alt=" " width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you click "Generate New Key", you'll need to type your key name into the text box, then click the blue check mark. After you click the blue check mark establishing your key name, you'll see these 2 new fields(&lt;strong&gt;save these for later&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%2Flzywk6kagtituaapnwe4.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%2Flzywk6kagtituaapnwe4.png" alt=" " width="800" height="147"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  DigitalOcean API Key
&lt;/h2&gt;

&lt;p&gt;Now that you have your access key and secret for the spaces bucket, you'll still need an API key to use with Terraform to create a few resources such as DigitalOcean droplets and a Spaces bucket. This can also be done in the "API" section of the 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%2F8b0gjx2479ne8qwqkolm.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%2F8b0gjx2479ne8qwqkolm.png" alt=" " width="800" height="218"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Terraform Code
&lt;/h3&gt;

&lt;p&gt;OK, so now we have our Spaces Access Key + Secret as well as our DigitalOcean API key. We can now move on to actually creating some droplets, a bucket, and share files across the two using s3fs.&lt;/p&gt;

&lt;p&gt;A quick overview of what is happening below. We're creating a new bucket and 2 droplets that will share files back and forth. This is done by taking some example input such as a region, mount point (filesystem path), and bucket name which will use &lt;a href="https://cloudinit.readthedocs.io/en/latest/" rel="noopener noreferrer"&gt;cloud-init&lt;/a&gt; to mount the bucket to the droplets when they first boot.&lt;/p&gt;

&lt;p&gt;First let's make a &lt;code&gt;terraform.tfvars&lt;/code&gt; file that takes in our configuration. It looks likes this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Spaces Access Key ID&lt;/span&gt;
&lt;span class="nx"&gt;spaces_access_key_id&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"XXX"&lt;/span&gt;
&lt;span class="c1"&gt;# Spaces Access Key Secret&lt;/span&gt;
&lt;span class="nx"&gt;spaces_access_key_secret&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"XXX"&lt;/span&gt;
&lt;span class="c1"&gt;# DigitalOcean API Token&lt;/span&gt;
&lt;span class="nx"&gt;do_token&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"XXX"&lt;/span&gt;
&lt;span class="c1"&gt;# SSH Key ID to be able to get into our new droplets (can leave this empty if no need to ssh)&lt;/span&gt;
&lt;span class="nx"&gt;ssh_key_id&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we need to create a file called &lt;code&gt;main.tf&lt;/code&gt; with the following content below. This will create our bucket and droplets and will configure s3fs on our droplets to be able to read and write files to the same bucket.&lt;/p&gt;

&lt;p&gt;Please refer to the comments for the walkthrough of each component:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Needed for terraform to initialize and&lt;/span&gt;
&lt;span class="c1"&gt;# install the digitalocean terraform provider&lt;/span&gt;
&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;digitalocean&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean/digitalocean"&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="c1"&gt;# Expected input, DigitalOcean Spaces Access Key ID&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"spaces_access_key_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&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="c1"&gt;# Expected input, DigitalOcean Spaces Access Key Secret&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"spaces_access_key_secret"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&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="c1"&gt;# Expected input, DigitalOcean API Token&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"do_token"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&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="c1"&gt;# SSH key in DigitalOcean that will allow us to get into our hosts&lt;/span&gt;
&lt;span class="c1"&gt;# (Not Necessarily Needed)&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"ssh_key_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;sensitive&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;default&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="c1"&gt;# DigitalOcean region to create our droplets and spaces bucket in&lt;/span&gt;
&lt;span class="c1"&gt;# Let's just go with nyc3&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"region"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nyc3"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Name of our DigitalOcean Spaces bucket&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"bucket_name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s3fs-bucket"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Where to mount our bucket on the filesystem on the DigitalOcean droplets&lt;/span&gt;
&lt;span class="c1"&gt;# Let's just default to /tmp/mount for demo purposes&lt;/span&gt;
&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"mount_point"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/mount"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Configure the DigitalOcean provider to create our resources&lt;/span&gt;
&lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;token&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;do_token&lt;/span&gt;
  &lt;span class="nx"&gt;spaces_access_id&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spaces_access_key_id&lt;/span&gt;
  &lt;span class="nx"&gt;spaces_secret_key&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;spaces_access_key_secret&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Create our DigitalOcean spaces bucket to store files&lt;/span&gt;
&lt;span class="c1"&gt;# that will be accessed by our droplets&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean_spaces_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"s3fs_bucket"&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="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bucket_name&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Let's create a sample file in the bucket called "index.html"&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean_spaces_bucket_object"&lt;/span&gt; &lt;span class="s2"&gt;"index"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digitalocean_spaces_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3fs_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digitalocean_spaces_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3fs_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;key&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"index.html"&lt;/span&gt;
  &lt;span class="nx"&gt;content&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;p&amp;gt;This page is empty.&amp;lt;/p&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;"&lt;/span&gt;
  &lt;span class="nx"&gt;content_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"text/html"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Configure our DigitalOcean droplets via cloud-init&lt;/span&gt;
&lt;span class="c1"&gt;# Install the s3fs package&lt;/span&gt;
&lt;span class="c1"&gt;# Create a system-wide credentials file for s3fs to be able to access the bucket&lt;/span&gt;
&lt;span class="c1"&gt;# Create a the mount point directory (/tmp/mount)&lt;/span&gt;
&lt;span class="c1"&gt;# Call s3fs to mount the bucket&lt;/span&gt;
&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;cloud_init_config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;yamlencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;packages&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"s3fs"&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="nx"&gt;write_files&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="nx"&gt;owner&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"root:root"&lt;/span&gt;
      &lt;span class="nx"&gt;path&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"/etc/passwd-s3fs"&lt;/span&gt;
      &lt;span class="nx"&gt;permissions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0600"&lt;/span&gt;
      &lt;span class="nx"&gt;content&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.spaces_access_key_id}:${var.spaces_access_key_secret}"&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
    &lt;span class="nx"&gt;runcmd&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s2"&gt;"mkdir -p ${var.mount_point}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"s3fs ${var.bucket_name} ${var.mount_point} -o url=https://${var.region}.digitaloceanspaces.com"&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="c1"&gt;# Convert our cloud-init config to userdata&lt;/span&gt;
&lt;span class="c1"&gt;# Userdata runs at first boot when the droplets are created&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"cloudinit_config"&lt;/span&gt; &lt;span class="s2"&gt;"server_config"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;gzip&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;base64_encode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;part&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;content_type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"text/cloud-config"&lt;/span&gt;
    &lt;span class="nx"&gt;content&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cloud_init_config&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Create 2 DigitalOcean droplets that will both mount the same spaces bucket&lt;/span&gt;
&lt;span class="c1"&gt;# These 2 hosts will share files back and forth&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean_droplet"&lt;/span&gt; &lt;span class="s2"&gt;"s3fs_droplet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ubuntu-20-04-x64"&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;"s3fs-droplet-${count.index}"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
  &lt;span class="nx"&gt;size&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"s-1vcpu-1gb"&lt;/span&gt;
  &lt;span class="nx"&gt;ssh_keys&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssh_key_id&lt;/span&gt; &lt;span class="err"&gt;!&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ssh_key_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cloudinit_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;server_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rendered&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Output our ip addresses to the console so that we can easily copy/pasta to ssh in&lt;/span&gt;
&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"s3fs_droplet_ipv4_addresses"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digitalocean_droplet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;s3fs_droplet&lt;/span&gt;&lt;span class="p"&gt;[*].&lt;/span&gt;&lt;span class="nx"&gt;ipv4_address&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Terraform Output
&lt;/h2&gt;

&lt;p&gt;Now that we have our configuration defined above, we simple need to run &lt;code&gt;terraform init &amp;amp;&amp;amp; terraform apply -auto-approve&lt;/code&gt; to create our things!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ terraform apply -auto-approve

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # digitalocean_droplet.s3fs_droplet[0] will be created
  + resource "digitalocean_droplet" "s3fs_droplet" {
      + backups              = false
      + created_at           = (known after apply)
      + disk                 = (known after apply)
      + graceful_shutdown    = false
      + id                   = (known after apply)
      + image                = "ubuntu-20-04-x64"
      + ipv4_address         = (known after apply)
      + ipv4_address_private = (known after apply)
      + ipv6                 = false
      + ipv6_address         = (known after apply)
      + locked               = (known after apply)
      + memory               = (known after apply)
      + monitoring           = false
      + name                 = "s3fs-droplet-0"
      + price_hourly         = (known after apply)
      + price_monthly        = (known after apply)
      + private_networking   = (known after apply)
      + region               = "nyc3"
      + resize_disk          = true
      + size                 = "s-1vcpu-1gb"
      + ssh_keys             = (sensitive)
      + status               = (known after apply)
      + urn                  = (known after apply)
      + user_data            = "dc35535cfb286b2994e31baa83c32ef808b9bdff"
      + vcpus                = (known after apply)
      + volume_ids           = (known after apply)
      + vpc_uuid             = (known after apply)
    }

  # digitalocean_droplet.s3fs_droplet[1] will be created
  + resource "digitalocean_droplet" "s3fs_droplet" {
      + backups              = false
      + created_at           = (known after apply)
      + disk                 = (known after apply)
      + graceful_shutdown    = false
      + id                   = (known after apply)
      + image                = "ubuntu-20-04-x64"
      + ipv4_address         = (known after apply)
      + ipv4_address_private = (known after apply)
      + ipv6                 = false
      + ipv6_address         = (known after apply)
      + locked               = (known after apply)
      + memory               = (known after apply)
      + monitoring           = false
      + name                 = "s3fs-droplet-1"
      + price_hourly         = (known after apply)
      + price_monthly        = (known after apply)
      + private_networking   = (known after apply)
      + region               = "nyc3"
      + resize_disk          = true
      + size                 = "s-1vcpu-1gb"
      + ssh_keys             = (sensitive)
      + status               = (known after apply)
      + urn                  = (known after apply)
      + user_data            = "dc35535cfb286b2994e31baa83c32ef808b9bdff"
      + vcpus                = (known after apply)
      + volume_ids           = (known after apply)
      + vpc_uuid             = (known after apply)
    }

  # digitalocean_spaces_bucket.s3fs_bucket will be created
  + resource "digitalocean_spaces_bucket" "s3fs_bucket" {
      + acl                = "private"
      + bucket_domain_name = (known after apply)
      + force_destroy      = false
      + id                 = (known after apply)
      + name               = "s3fs-bucket"
      + region             = "nyc3"
      + urn                = (known after apply)
    }

  # digitalocean_spaces_bucket_object.index will be created
  + resource "digitalocean_spaces_bucket_object" "index" {
      + acl           = "private"
      + bucket        = "s3fs-bucket"
      + content       = "&amp;lt;html&amp;gt;&amp;lt;body&amp;gt;&amp;lt;p&amp;gt;This page is empty.&amp;lt;/p&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;"
      + content_type  = "text/html"
      + etag          = (known after apply)
      + force_destroy = false
      + id            = (known after apply)
      + key           = "index.html"
      + region        = "nyc3"
      + version_id    = (known after apply)
    }

Plan: 4 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + s3fs_droplet_ipv4_addresses = [
      + (known after apply),
      + (known after apply),
    ]
digitalocean_spaces_bucket.s3fs_bucket: Creating...
digitalocean_droplet.s3fs_droplet[1]: Creating...
digitalocean_droplet.s3fs_droplet[0]: Creating...
digitalocean_spaces_bucket.s3fs_bucket: Still creating... [10s elapsed]
digitalocean_droplet.s3fs_droplet[1]: Still creating... [10s elapsed]
digitalocean_droplet.s3fs_droplet[0]: Still creating... [10s elapsed]
digitalocean_droplet.s3fs_droplet[1]: Still creating... [20s elapsed]
digitalocean_droplet.s3fs_droplet[0]: Still creating... [20s elapsed]
digitalocean_spaces_bucket.s3fs_bucket: Still creating... [20s elapsed]
digitalocean_spaces_bucket.s3fs_bucket: Creation complete after 28s [id=s3fs-bucket]
digitalocean_spaces_bucket_object.index: Creating...
digitalocean_spaces_bucket_object.index: Creation complete after 0s [id=index.html]
digitalocean_droplet.s3fs_droplet[0]: Still creating... [30s elapsed]
digitalocean_droplet.s3fs_droplet[1]: Still creating... [30s elapsed]
digitalocean_droplet.s3fs_droplet[0]: Still creating... [40s elapsed]
digitalocean_droplet.s3fs_droplet[1]: Still creating... [40s elapsed]
digitalocean_droplet.s3fs_droplet[1]: Creation complete after 43s [id=283287872]
digitalocean_droplet.s3fs_droplet[0]: Creation complete after 43s [id=283287873]

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

s3fs_droplet_ipv4_addresses = [
  "165.227.106.47",
  "45.55.60.230",
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Cool! Now have our bucket and some droplets already configured, let's ssh to both and checkout that &lt;code&gt;/tmp/mount&lt;/code&gt; path that we set up in our Terraform configuration above.&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%2Fmynxgmv7e72ajxphwhqn.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%2Fmynxgmv7e72ajxphwhqn.png" alt=" " width="800" height="179"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's do a recap of what's happening above.&lt;/p&gt;

&lt;p&gt;On both &lt;code&gt;s3fs-droplet-0&lt;/code&gt; and &lt;code&gt;s3fs-droplet-1&lt;/code&gt;, I ran &lt;code&gt;df -h | grep s3fs&lt;/code&gt; which gives us our disk usage for all of the mounted volumes, but I filtered specifically for the term &lt;code&gt;s3fs&lt;/code&gt; to shorten the list. This shows us that our bucket is mounted and available at &lt;code&gt;/tmp/mount&lt;/code&gt;! Hooray!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@s3fs-droplet-0:/tmp/mount#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;s3fs
&lt;span class="go"&gt;s3fs            256T     0  256T   0% /tmp/mount

&lt;/span&gt;&lt;span class="gp"&gt;root@s3fs-droplet-1:/tmp/mount#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;s3fs
&lt;span class="go"&gt;s3fs            256T     0  256T   0% /tmp/mount
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I ran &lt;code&gt;ll /tmp/mount&lt;/code&gt; on both hosts so that we can see that the contents of the bucket and we can see the &lt;code&gt;index.html&lt;/code&gt; file that I created in the bucket in the Terraform code is there and is viewable by both droplets. Awesooooome!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@s3fs-droplet-0:/tmp/mount#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ll /tmp/mount/
&lt;span class="go"&gt;total 5
drwx------  1 root root    0 Jan  1  1970 ./
drwxrwxrwt 12 root root 4096 Jan 22 18:54 ../
-rw-r-----  1 root root   52 Jan 22 18:48 index.html

&lt;/span&gt;&lt;span class="gp"&gt;root@s3fs-droplet-1:/tmp/mount#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ll /tmp/mount/
&lt;span class="go"&gt;total 5
drwx------  1 root root    0 Jan  1  1970 ./
drwxrwxrwt 12 root root 4096 Jan 22 18:54 ../
-rw-r-----  1 root root   52 Jan 22 18:48 index.html
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;OK, so next I ran a &lt;code&gt;touch&lt;/code&gt; command on &lt;code&gt;s3fs-droplet-0&lt;/code&gt; which created a file in &lt;code&gt;/tmp/mount&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@s3fs-droplet-0:/tmp/mount#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;file_from_&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I used &lt;code&gt;$(hostname)&lt;/code&gt; to substitute the name of the droplet in the file name so that we can see said file on &lt;code&gt;s3fs-droplet-1&lt;/code&gt;. Let's have a look and see if that file is viewable on the other server.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;root@s3fs-droplet-1:/tmp/mount#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ll /tmp/mount/
&lt;span class="go"&gt;total 6
drwx------  1 root root    0 Jan  1  1970 ./
drwxrwxrwt 12 root root 4096 Jan 22 18:54 ../
-rw-r--r--  1 root root    0 Jan 22 19:00 file_from_s3fs-droplet-0
-rw-r-----  1 root root   52 Jan 22 18:48 index.html
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's there! We successfully shared files between our 2 droplets. Now let's go look at our spaces bucket in the DigitalOcean 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%2Fxm2vamsho0hdlo44c3u8.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%2Fxm2vamsho0hdlo44c3u8.png" alt=" " width="800" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;WOOT WOOT! Since we're using the spaces bucket, we can access these files from anywhere and in any application! NFS is looking pretty gross at this point. Yay for cloud object storage and thanks to DigitalOcean for providing us such a cool service!&lt;/p&gt;

&lt;p&gt;Fin.&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>terraform</category>
      <category>tutorial</category>
      <category>tooling</category>
    </item>
    <item>
      <title>The time I had night terrors about us-east-1 outages</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Tue, 28 Dec 2021 15:33:28 +0000</pubDate>
      <link>https://forem.com/circa10a/the-time-i-had-night-terrors-about-us-east-1-outages-5d1b</link>
      <guid>https://forem.com/circa10a/the-time-i-had-night-terrors-about-us-east-1-outages-5d1b</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I normally never write about personal things, only technical things. However this is one of those times that I feel this is too good not to write down and I need to write this all down now before I forget.&lt;/p&gt;

&lt;p&gt;As the title suggests, I just woke up in a cold sweat from an incredibly awkward nightmare thanks to all the AWS us-east-1 outages as of late, here's the story.&lt;/p&gt;

&lt;h2&gt;
  
  
  The nightmare
&lt;/h2&gt;

&lt;p&gt;It started as me being back in college going to classes, but for some reason, these college classes were at my high school. Because dreams always make sense. I was in the middle of class and I fall out of my desk very suddenly and I remember feeling very disoriented. I can't get up. People are staring. Finally some kind soul helps me out of the classroom and it just hits me. I'm behaving so strangely because obviously I'm in a simulation. It was obvious to me in the dream that it couldn't be real because well 1. I was in college at my high school and 2. I never just collapse on my own. I think this is what is called &lt;a href="https://en.wikipedia.org/wiki/Lucid_dream" rel="noopener noreferrer"&gt;lucid dreaming&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next I'm sitting outside of the classroom in the hallway, still disoriented, but then it goes next level. I start losing my shit. I start taking off my clothes and a neighboring classroom has a teacher giving some standardized test. So naturally she notices me wigging out and brings me a copy of the standardized test to do (because dreams make sense). I'll be damned if I do another one of those tests, so I yeet on out of there half-naked.&lt;/p&gt;

&lt;p&gt;As I'm wandering through the halls of school half-dressed, the worst thing that could possibly happen... it happens. &lt;strong&gt;The bell rings&lt;/strong&gt;. All of my peers then walk past me looking at me like I'm a freak show. Then it clicks. The simulation that I'm in is running in us-east-1. I had to tell someone. So what do I do? Grab a bystander of course and yell in their face "EBS is experiencing degraded performance!" I mean it made sense right? Why else would I be looking, feeling, and acting this way.&lt;/p&gt;

&lt;p&gt;It keeps going. I'm still wandering the halls trying to find my next class. I can't. I can't for the life of me find where this stupid classroom is. I knew my theory was correct because I went to this school for 4 years. I knew where everything was. I would only make sense that my memory is shit because EBS (cloud storage) isn't working.&lt;/p&gt;

&lt;p&gt;I make it to my next class eventually and sit down. And wouldn't you know it, my old college professor Dr. Russell is there teaching... in my high school.. where I was taking college classes. Yeah. Anyway, I realize I'm still way too disoriented to sit through this class, so back to the halls I go... still half-dressed.&lt;/p&gt;

&lt;p&gt;Then I see my girlfriend (now wife). I thought if anybody could help me or set me straight, it's her. So I run up to her and couldn't even call her by her name. I call her "mom" thanks to the degraded cloud storage powering my horrid simulation brain. She then responds to me and calls me "Mr..." something. &lt;/p&gt;

&lt;p&gt;It was at this point that I knew everything was absolutely as bad as it could be. My own girlfriend didn't know who I was. I didn't even remember her name. I'm in the hallway half-naked. It was then that my virtual world started crashing down around me. I remember falling backwards into the abyss much like Alice in Wonderland. All thanks to us-east-1 being down and not being able to power my simulation.&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%2Fj5aoezfks7i69ftnqvg9.gif" 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%2Fj5aoezfks7i69ftnqvg9.gif" alt="alt text" width="400" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks AWS.&lt;/p&gt;

&lt;p&gt;If it's any consolation, I did just go see The Matrix Resurrections&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>discuss</category>
      <category>aws</category>
      <category>writing</category>
    </item>
    <item>
      <title>Ephemeral Jenkins Users + API Tokens using Hashicorp Vault</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Mon, 27 Dec 2021 21:24:18 +0000</pubDate>
      <link>https://forem.com/circa10a/ephemeral-jenkins-users-api-tokens-using-hashicorp-vault-49kn</link>
      <guid>https://forem.com/circa10a/ephemeral-jenkins-users-api-tokens-using-hashicorp-vault-49kn</guid>
      <description>&lt;p&gt;New year, new credentials! (well almost)&lt;/p&gt;

&lt;h2&gt;
  
  
  The Jenkins rabbit hole
&lt;/h2&gt;

&lt;p&gt;Not long ago, I (like most people), went down the rabbit hole of working around some Jenkins limitations. I was experiencing some issues of hitting identity provider API rate limits due to heavy API usage of Jenkins. After some experimenting, I found that using Jenkins API tokens for machine to machine communication instead of user credentials doesn't require identity provider authentication which significantly cuts down on hitting said rate limits. "This is great!", I thought.&lt;/p&gt;

&lt;h2&gt;
  
  
  SDK's... those darn SDK's...
&lt;/h2&gt;

&lt;p&gt;Next up, I was looking into the the &lt;a href="https://github.com/bndr/gojenkins" rel="noopener noreferrer"&gt;gojenkins&lt;/a&gt; client library to see if there were some built-in methods to allow me to do my own API token management and rotation to solve the issue above. And of course, it didn't exist. It was at this point I sat back and thought, "Really? No one has yet to implement a decent solution for managing Jenkins tokens/users yet? This is the most widely used CI server out there. Surely it has to exist..."&lt;/p&gt;

&lt;p&gt;Sure, I was already aware of the &lt;a href="https://plugins.jenkins.io/hashicorp-vault-plugin/" rel="noopener noreferrer"&gt;vault-jenkins-plugin&lt;/a&gt; which allowed Jenkins to fetch secrets from Vault, but what about accessing Jenkins itself?! Nope, it didn't exist. I then began to think about all the work needed to solve this problem in a non-crufty way. First of which included some PR's to the gojenkins client library to add some API/User management methods to make the overall solution cleaner, as well as give back to the community so that they can automate all of their problems away as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time to do the needful
&lt;/h2&gt;

&lt;p&gt;I've built other plugins for products within the Hashicorp ecosystem before, such as Terraform, but never a Vault plugin. I thought this would be a good opportunity to learn more about the inner workings of Vault and fill this gap in the community. &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%2F9gy97hs4zbbnhhlgtd04.gif" 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%2F9gy97hs4zbbnhhlgtd04.gif" alt="alt text" width="480" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Low and behold, I created the &lt;a href="https://github.com/circa10a/vault-plugin-secrets-jenkins" rel="noopener noreferrer"&gt;vault-plugin-secrets-jenkins&lt;/a&gt; project!&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Yes. That is a glorious chicken sandwich and I'm leaving it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  So what does this thingy do?
&lt;/h2&gt;

&lt;p&gt;I'm glad you asked!&lt;/p&gt;

&lt;p&gt;Once installed, the vault plugin takes in a configuration of a "root" Jenkins user. Then on behalf of this user, Vault can create short lived users as well as short lived API tokens for the configured user!&lt;/p&gt;

&lt;h2&gt;
  
  
  But why?
&lt;/h2&gt;

&lt;p&gt;Dude, it's almost 2022. Hardcoding credentials as environment variables is not the best way to handle secrets these days. Now you can just request/rotate your own dynamic credentials by integrating with Vault.&lt;/p&gt;

&lt;h2&gt;
  
  
  So what does it look like to use this thing?
&lt;/h2&gt;

&lt;p&gt;First detailed guidance/examples, you'll want to checkout the project README &lt;a href="https://github.com/circa10a/vault-plugin-secrets-jenkins" rel="noopener noreferrer"&gt;here&lt;/a&gt;, but while I have you, I'll show you some examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enable the plugin
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault secrets &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;-path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;jenkins vault-plugin-secrets-jenkins
Success! Enabled the vault-plugin-secrets-jenkins secrets engine at: jenkins/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Configure the plugin
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault write jenkins/config &lt;span class="nv"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://localhost:8080 &lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin
Success! Data written to: jenkins/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Bonus: The plugin validates your config before moving on 😁&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create a short lived API token
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault &lt;span class="nb"&gt;read &lt;/span&gt;jenkins/tokens/mytoken
Key                Value
&lt;span class="nt"&gt;---&lt;/span&gt;                &lt;span class="nt"&gt;-----&lt;/span&gt;
lease_id           jenkins/tokens/mytoken/fJ57afQZMyXDcJnm74BgLLt8
lease_duration     5m
lease_renewable    &lt;span class="nb"&gt;true
&lt;/span&gt;token              1184cb7b22c404efa1c293e9841b66f345
token_id           1c2864f3-4108-4417-807a-358357bc8432
token_name         mytoken
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create a short lived user
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vault write jenkins/users/myuser &lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;password &lt;span class="nv"&gt;fullname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Jenkins the Butler"&lt;/span&gt; &lt;span class="nv"&gt;email&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;email@example.com
Key                Value
&lt;span class="nt"&gt;---&lt;/span&gt;                &lt;span class="nt"&gt;-----&lt;/span&gt;
lease_id           jenkins/users/myuser/hTGbJhDFbAQpALv1FjJyJ4vz
lease_duration     5m
lease_renewable    &lt;span class="nb"&gt;true
&lt;/span&gt;email              email@example.com
fullname           Jenkins the Butler
username           myuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  So what now?
&lt;/h2&gt;

&lt;p&gt;Woohoo! We now have automatically expiring users and tokens we can manage from our app and can give our pals over in information security some piece of mind that even if our credentials do leak, they'll be useless within just a couple of minutes. Pretty slick right?!&lt;/p&gt;

&lt;h2&gt;
  
  
  Fin
&lt;/h2&gt;

&lt;p&gt;Thanks for taking the time to checkout my article!&lt;/p&gt;

&lt;p&gt;(gifs of more chicken sandwiches are encouraged in the comments section)&lt;/p&gt;

</description>
      <category>devops</category>
      <category>go</category>
      <category>security</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Geofence your self-hosted web server and API's</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Sat, 27 Nov 2021 19:45:00 +0000</pubDate>
      <link>https://forem.com/circa10a/geofence-your-self-hosted-apis-4jfn</link>
      <guid>https://forem.com/circa10a/geofence-your-self-hosted-apis-4jfn</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Not long ago, &lt;a href="https://dev.to/circa10a/interactive-halloween-decorations-with-raspberry-pis-24el"&gt;I made a post about my home-hosted interactive Halloween system&lt;/a&gt;. It's basically a custom built REST API to interact with a bunch of smart things, but needed to be hosted at home to more easily interact with devices on my local network. I then exposed my system to the world, but the intent was to only have folks on my street be able to interact with my Halloween decorations. &lt;br&gt;
It's no fun when folks from across my neighborhood hear about it, go to the site and trigger a bunch effects, and no one be there to enjoy. The other scenario is that I have friends on the other side of the country mess with me by having my lights flash red and blue 🚨 on repeat (looking at you Traci).&lt;/p&gt;
&lt;h2&gt;
  
  
  Geofencing
&lt;/h2&gt;

&lt;p&gt;What did I do to solve this? I implemented something called IP geofencing on my self-hosted API.&lt;/p&gt;

&lt;p&gt;So what is geofencing?&lt;/p&gt;

&lt;p&gt;IP geofencing is a security measure that restricts IP availability by geography, regardless of a user's access permissions. Think of it as a virtual perimeter around a given location.&lt;/p&gt;
&lt;h3&gt;
  
  
  But how?
&lt;/h3&gt;

&lt;p&gt;I wrote a library called &lt;a href="https://github.com/circa10a/go-geofence" rel="noopener noreferrer"&gt;go-geofence&lt;/a&gt; that uses &lt;a href="https://ipbase.com/" rel="noopener noreferrer"&gt;ipbase.com&lt;/a&gt; behind these scenes because the service gives you 150 requests per month for free and 65,000 per month for $10.&lt;/p&gt;
&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;I wanted something free and simple. Here's some example usage of the Go library I wrote below. All you need is an API key from &lt;a href="https://ipbase.com/" rel="noopener noreferrer"&gt;ipbase.com&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When the client is instantiated, it uses &lt;a href="https://ipbase.com/" rel="noopener noreferrer"&gt;ipbase.com&lt;/a&gt; to determine the GPS coordinates of the client running the software. Then when the &lt;code&gt;IsIPAddressNear()&lt;/code&gt; method is called, the library then looks up the GPS coordinates of the IP address specified, then compares the coordinates and computes distance in kilometers and determines if the IP address' location is close to yours.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"log"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;

    &lt;span class="s"&gt;"github.com/circa10a/go-geofence"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;geofence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;geofence&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;New&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;geofence&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c"&gt;// Empty string to geofence your current public IP address, or you can monitor a remote address by supplying it as the first parameter&lt;/span&gt;
        &lt;span class="n"&gt;IPAddress&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c"&gt;// ipbase.com API token&lt;/span&gt;
        &lt;span class="n"&gt;Token&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"YOUR_IPBASE_API_TOKEN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="c"&gt;// Maximum radius of the geofence in kilometers, only clients less than or equal to this distance will return true with isAddressNearby&lt;/span&gt;
        &lt;span class="c"&gt;// 1 kilometer&lt;/span&gt;
        &lt;span class="n"&gt;Radius&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;CacheTTL&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="m"&gt;7&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Hour&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c"&gt;// 1 week&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;isAddressNearby&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;geofence&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsIPAddressNear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"8.8.8.8"&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;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="c"&gt;// Address nearby: false&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Address nearby: "&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isAddressNearby&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;h2&gt;
  
  
  How was it implemented?
&lt;/h2&gt;

&lt;p&gt;Since I'm using &lt;a href="https://echo.labstack.com/" rel="noopener noreferrer"&gt;echo&lt;/a&gt; as the web framework to control my decorations, I was able to implement some &lt;a href="https://github.com/circa10a/witchonstephendrive.com/blob/main/routes/middleware/geofencing/geofencing.go" rel="noopener noreferrer"&gt;pretty simple middleware&lt;/a&gt; that rejects &lt;code&gt;POST&lt;/code&gt; requests from IP addresses that aren't within close proximity to mine with a &lt;code&gt;403&lt;/code&gt; status code.&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%2Ftdghtb055rwp0qnkq2vv.gif" 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%2Ftdghtb055rwp0qnkq2vv.gif" alt="alt text" width="480" height="270"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  I want to implement the same thing for my home web server/API, but don't use Go
&lt;/h2&gt;

&lt;p&gt;Well, boy do I have the solution for you!&lt;/p&gt;

&lt;p&gt;I was surprised with the lack of open source geofencing solutions for this when I was originally looking into it. So to make the implementation more accessible, I built a module for the open source web server called &lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caddy
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;Caddy&lt;/a&gt; is an &lt;strong&gt;awesome&lt;/strong&gt; web server alternative to &lt;a href="https://www.nginx.com/" rel="noopener noreferrer"&gt;nginx&lt;/a&gt; and &lt;a href="https://httpd.apache.org/" rel="noopener noreferrer"&gt;apache (httpd)&lt;/a&gt;. Caddy is written in Go, is a much more performant and extensible web server (in my opinion). With Caddy, you can host basic files or reverse proxy your API's, which is how I use it. I use Caddy because of its automatic TLS functionality so I don't have to worry about manual creation of certificates and keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  caddy-geofence
&lt;/h2&gt;

&lt;p&gt;To integrate with Caddy, I wrote a &lt;a href="https://github.com/circa10a/caddy-geofence" rel="noopener noreferrer"&gt;geofencing module&lt;/a&gt; than can be used with a wealth of configuration options thanks to Caddy's versatile abilities. You can configure the module per route, request method, pretty much any condition you can think of.&lt;/p&gt;

&lt;p&gt;To implement the same geofencing functionality that I did to restrict who can use your HTTP services, all you need is an API key from &lt;a href="https://ipbase.com/" rel="noopener noreferrer"&gt;ipbase.com&lt;/a&gt; and create a new web server with a &lt;a href="https://caddyserver.com/docs/caddyfile" rel="noopener noreferrer"&gt;Caddyfile&lt;/a&gt; to specify your token and configuration preferences.&lt;/p&gt;

&lt;p&gt;If you're new to Caddy, one thing to note about Caddy and installing modules is that modules are compiled with Caddy, thus resulting in a single binary to execute.&lt;/p&gt;

&lt;p&gt;Here's an example Caddyfile you would use to configure Caddy with geofencing functionality:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
    order geofence before respond
}

:80

route /* {
    geofence {
        # cache_ttl is the duration to store ip addresses and if they are within proximity or not to increase performance
        # Cache for 7 days, valid time units are "ms", "s", "m", "h"
        # Not specifying a TTL sets no expiration on cached items and will live until restart
        cache_ttl 168h

        # ipbase.com API token, this example reads from an environment variable
        ipbase_api_token {$IPBASE_API_TOKEN}

        # radius is the the distance of the geofence, only clients within the distance will be allowed.
        # If not supplied, will default to 0.0 kilometers
        radius 1.0

        # allow_private_ip_addresses is a boolean for whether or not to allow private ip ranges
        # such as 192.X, 172.X, 10.X, [::1] (localhost)
        # false by default
        # Some cellular networks doing NATing with 172.X addresses, in which case, you may not want to allow
        allow_private_ip_addresses true

        # allowlist is a list of IP addresses that will not be checked for proximity and will be allowed to access the server
        allowlist 206.189.205.251 206.189.205.252

        # status_code is the HTTP response code that is returned if IP address is not within proximity. Default is 403
        status_code 403
    }
}

log {
    output stdout
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then to run Caddy with the module installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--net&lt;/span&gt; host &lt;span class="nt"&gt;-v&lt;/span&gt; /your/Caddyfile:/etc/caddy/Caddyfile &lt;span class="nt"&gt;-e&lt;/span&gt; IPBASE_API_TOKEN &lt;span class="nt"&gt;-p&lt;/span&gt; 80:80 &lt;span class="nt"&gt;-p&lt;/span&gt; 443:443 circa10a/caddy-geofence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if if you would rather compile Caddy yourself with the module and not use docker, you can use &lt;a href="https://caddyserver.com/docs/build#xcaddy" rel="noopener noreferrer"&gt;xcaddy&lt;/a&gt; like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcaddy build &lt;span class="nt"&gt;--with&lt;/span&gt; github.com/circa10a/caddy-geofence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more info on implementing a geofenced web server/reverse proxy with caddy, see the &lt;a href="https://github.com/circa10a/caddy-geofence" rel="noopener noreferrer"&gt;caddy-geofence repo&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Fin
&lt;/h2&gt;

&lt;p&gt;And there you have it, expose your web server or API's and limit who can use them based on proximity to your physical location.&lt;/p&gt;

</description>
      <category>go</category>
      <category>webdev</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Pull Requests need more cute animals</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Tue, 09 Nov 2021 04:14:43 +0000</pubDate>
      <link>https://forem.com/circa10a/pull-requests-need-more-cute-animals-3oi0</link>
      <guid>https://forem.com/circa10a/pull-requests-need-more-cute-animals-3oi0</guid>
      <description>&lt;h3&gt;
  
  
  My Workflow
&lt;/h3&gt;

&lt;p&gt;I created a new GitHub action called &lt;a href="https://github.com/circa10a/animal-action" rel="noopener noreferrer"&gt;animal-action&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When a pull request is opened to a repository, it will comment with a cute picture of either a cat, dog, or fox!&lt;/p&gt;

&lt;p&gt;The types of animals and comment supplied are completely configurable.&lt;/p&gt;

&lt;p&gt;Current animals supported:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;cats 🐈&lt;/li&gt;
&lt;li&gt;dogs 🐕&lt;/li&gt;
&lt;li&gt;foxes 🦊&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Example
&lt;/h4&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%2Fe82pku6lw118m0pn73u7.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%2Fe82pku6lw118m0pn73u7.png" alt="alt text" width="800" height="580"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt; Wacky Wildcards&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Yaml File or 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/circa10a" rel="noopener noreferrer"&gt;
        circa10a
      &lt;/a&gt; / &lt;a href="https://github.com/circa10a/animal-action" rel="noopener noreferrer"&gt;
        animal-action
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A github action to add smiles to pull requests
    &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;animal-action&lt;/h1&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/circa10a/animal-action/workflows/release/badge.svg"&gt;&lt;img src="https://github.com/circa10a/animal-action/workflows/release/badge.svg" alt="Build Status"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/fa4caead0fe265e23e56178f586b20f63adb27de0f76f79e4fa539bdaa1f46f2/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f63697263613130612f616e696d616c2d616374696f6e3f7374796c653d706c6173746963"&gt;&lt;img src="https://camo.githubusercontent.com/fa4caead0fe265e23e56178f586b20f63adb27de0f76f79e4fa539bdaa1f46f2/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f762f72656c656173652f63697263613130612f616e696d616c2d616374696f6e3f7374796c653d706c6173746963" alt="GitHub release (latest by date)"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A github action to add smiles to pull requests&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/circa10a/animal-action/docs/img/example.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%2Fcirca10a%2Fanimal-action%2Fdocs%2Fimg%2Fexample.png" alt="alt text"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Inputs&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;&lt;code&gt;github_token&lt;/code&gt;&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Required&lt;/strong&gt; A GitHub token&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;&lt;code&gt;animals&lt;/code&gt;&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;A comma-delimated string of types of animals pictures to comment with.&lt;/p&gt;
&lt;p&gt;Default: &lt;code&gt;"cats,dogs,foxes"&lt;/code&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;&lt;code&gt;pull_request_comment&lt;/code&gt;&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;Comment to post along with animal picture.&lt;/p&gt;
&lt;p&gt;Default: &lt;code&gt;':tada: Thank you for your contribution! While we review, please enjoy this cute animal picture'&lt;/code&gt;.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Outputs&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;None&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Example usage&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-yaml notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-ent"&gt;name&lt;/span&gt;: &lt;span class="pl-s"&gt;comment&lt;/span&gt;
&lt;span class="pl-ent"&gt;on&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;pull_request&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;types&lt;/span&gt;: &lt;span class="pl-s"&gt;[opened]&lt;/span&gt;
&lt;span class="pl-ent"&gt;jobs&lt;/span&gt;:
  &lt;span class="pl-ent"&gt;comment&lt;/span&gt;:
    &lt;span class="pl-ent"&gt;runs-on&lt;/span&gt;: &lt;span class="pl-s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="pl-ent"&gt;steps&lt;/span&gt;:
      - &lt;span class="pl-ent"&gt;uses&lt;/span&gt;: &lt;span class="pl-s"&gt;circa10a/animal-action@main&lt;/span&gt;
        &lt;span class="pl-ent"&gt;with&lt;/span&gt;:
          &lt;span class="pl-ent"&gt;github_token&lt;/span&gt;: &lt;span class="pl-s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
          &lt;span class="pl-ent"&gt;animals&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;cats,dogs&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; cats,dogs,foxes currently supported&lt;/span&gt;
          &lt;span class="pl-ent"&gt;pull_request_comment&lt;/span&gt;: &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;🎉 Thank you for the contribution! Here&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;&lt;span class="pl-s"&gt;s a cute animal picture to say thank you!'&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;/div&gt;



&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/circa10a/animal-action" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


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

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;More smiles in Open Source is a definite win. 😄&lt;/p&gt;

</description>
      <category>actionshackathon21</category>
      <category>javascript</category>
      <category>github</category>
    </item>
    <item>
      <title>Interactive Halloween decorations with raspberry pi's 🎃</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Sun, 31 Oct 2021 20:04:46 +0000</pubDate>
      <link>https://forem.com/circa10a/interactive-halloween-decorations-with-raspberry-pis-24el</link>
      <guid>https://forem.com/circa10a/interactive-halloween-decorations-with-raspberry-pis-24el</guid>
      <description>&lt;h2&gt;
  
  
  Video Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=UTl32JWIu6o" rel="noopener noreferrer"&gt;Link to demo video&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;Last year (2020), I decided to do something fun for my neighborhood due to the dark and shitty times that is COVID. I was convinced that kids wouldn't have a normal Halloween for trick or treating. So I built a thing. The idea was to have outdoor lighting with a mobile friendly web app that folks could use and change the colors of my outdoor Halloween lighting to the color/effect of their choice.&lt;/p&gt;

&lt;p&gt;I ended up calling the project/site witchonstephendrive.com since I had this pretty slick witch silhouette curtain in the front bedroom window of my house and my street is Stephen Drive of course.&lt;/p&gt;

&lt;p&gt;Anyways, here's what it looks like outside of my house with blue/green colors set.&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%2Fo41s64i9leon0rwm97k6.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%2Fo41s64i9leon0rwm97k6.jpg" alt=" " width="" height=""&gt;&lt;/a&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%2Fmehvkbgess7vdlrxvfii.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%2Fmehvkbgess7vdlrxvfii.jpg" alt=" " width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Mobile app
&lt;/h2&gt;

&lt;p&gt;I put a sign in my front yard to tell folks where to go to on their phones to do the needful. Here's a preview of what the site 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%2Fksjbyvozzuzsv8g2fwny.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%2Fksjbyvozzuzsv8g2fwny.png" width="694" height="1284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What all does it do?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Lights
&lt;/h3&gt;

&lt;p&gt;I have 5 philips hue lights in the front that are controlled via the app. There are several colors to choose from individually or you can select a rainbow effect or the "flash" effect which turns all of the lights off then back on a few times for a spooky effect.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sounds
&lt;/h3&gt;

&lt;p&gt;When a user selects presses a button to change the lights, a spooky sound gets played via a google assistant connected speaker.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backend
&lt;/h3&gt;

&lt;p&gt;I wrote a Go based REST API with lights/sounds endpoints that my front end interacts with.&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%2F3a6rffqppn8ht9mu3lgl.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%2F3a6rffqppn8ht9mu3lgl.png" alt=" " width="800" height="562"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Lights
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;/colors/:color&lt;/code&gt; endpoint interacts with the hue bridge to set color/brightness state using a &lt;a href="https://github.com/amimof/huego" rel="noopener noreferrer"&gt;3rd party philips hue sdk&lt;/a&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Sounds
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;/sounds/:sound&lt;/code&gt; endpoint interacts with the google assistant speaker using &lt;a href="https://www.home-assistant.io/" rel="noopener noreferrer"&gt;home assistant&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Sounds are processed using a configurable in-memory queue to ensure sounds aren't interrupted and play all the way through. It would suck to have kids smashing buttons and you would never get to hear a full mp3 play.&lt;/p&gt;

&lt;h3&gt;
  
  
  Front end
&lt;/h3&gt;

&lt;p&gt;The front end web app for this site is just a vanilla html/css/js that interacts with the REST API, but here's the cool part. Since Go 1.16 started supporting embedding files into the compiled binary, the front end and backend are both served via a single binary which makes deployment of this application super simple.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hosting
&lt;/h3&gt;

&lt;p&gt;This app is just hosted on a simple raspberry pi 4 in my TV stand running in a docker container. It's ideal to host this on your local network to auto discover the hue bridge. No cloud or kubernetes needed.&lt;/p&gt;

&lt;p&gt;All that was needed was to forward port 80/443 from my router to my raspberry pi's local ip address.&lt;/p&gt;

&lt;h3&gt;
  
  
  Notable Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Schedules

&lt;ul&gt;
&lt;li&gt;Turning lights on/off at configurable times.&lt;/li&gt;
&lt;li&gt;Setting individual lights back to default colors at configurable times.&lt;/li&gt;
&lt;li&gt;Bridge IP address renewal at configurable interval to ensure DHCP doesn't change the address&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Quiet time

&lt;ul&gt;
&lt;li&gt;Sounds supports configurable start and end hours to not play sounds. Don't want to be a 🍆 to the neighbors by having sounds blaring at all hours of the night.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Monitoring

&lt;ul&gt;
&lt;li&gt;Prometheus metrics built-in to monitor color selection usage frequency.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Rate limiting

&lt;ul&gt;
&lt;li&gt;I'm using &lt;a href="https://caddyserver.com/" rel="noopener noreferrer"&gt;caddy&lt;/a&gt; for my web server due to it's awesome automatic certificate functionality using &lt;a href="https://letsencrypt.org/" rel="noopener noreferrer"&gt;Let's Encrypt&lt;/a&gt; behind the scenes. Caddy supports rate limiting via &lt;a href="https://github.com/mholt/caddy-ratelimit" rel="noopener noreferrer"&gt;this plugin&lt;/a&gt; so I don't have to worry about folks killing my API.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;IP Geofencing&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://github.com/circa10a/witchonstephendrive.com/#terraform-module" rel="noopener noreferrer"&gt;Terraform module to interact with lights/sounds&lt;/a&gt; ...because why not.&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's that cool pumpkin under the tree that watches you in the video?
&lt;/h2&gt;

&lt;p&gt;That's my &lt;a href="https://github.com/circa10a/pumpkin-pi" rel="noopener noreferrer"&gt;pumpkin-pi&lt;/a&gt; project that I also created using another raspberry pi! It uses 2 motion sensors and a servo motor to detect people walking on the sideway and turns the pumpkin towards them to simulate it "watching" you. Check out the repo to see how to build your own!&lt;/p&gt;

&lt;p&gt;&lt;a href="http://www.youtube.com/watch?v=fl52GQJCFVI" rel="noopener noreferrer"&gt;Here is a video demo close up&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Source code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/circa10a/witchonstephendrive.com" rel="noopener noreferrer"&gt;Here's a link to the project&lt;/a&gt; if you'd like to deploy your own next year!&lt;/p&gt;

</description>
      <category>halloween</category>
      <category>go</category>
      <category>showdev</category>
      <category>hacktoberfest</category>
    </item>
    <item>
      <title>Monitoring GitHub Pull Requests with Prometheus</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Tue, 10 Aug 2021 23:13:06 +0000</pubDate>
      <link>https://forem.com/circa10a/monitoring-github-pull-requests-with-prometheus-57p2</link>
      <guid>https://forem.com/circa10a/monitoring-github-pull-requests-with-prometheus-57p2</guid>
      <description>&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Have you ever wanted to track your open source contributions? Or perhaps monitor contributions made by multiple users for some sort of event? Well I had the exact the same problem 😊. &lt;br&gt;
With &lt;a href="https://hacktoberfest.digitalocean.com/" rel="noopener noreferrer"&gt;Hacktoberfest&lt;/a&gt; just around the corner, I wanted a way to automatically track open source contributions to be able to incentivize participation via prizes or similar. It's fairly difficult to track who opens pull requests to what projects on GitHub at scale within a large organization, but boy do I have the solution for you.&lt;/p&gt;
&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;I built a &lt;a href="https://github.com/circa10a/github-pr-exporter" rel="noopener noreferrer"&gt;prometheus exporter&lt;/a&gt; that takes in a config file with a list of users and will use the GitHub search API to find pull requests created by said users. This exporter exposes some useful data such as &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;created_at&lt;/code&gt;, &lt;code&gt;status&lt;/code&gt; and &lt;code&gt;link&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  What is a prometheus exporter?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://prometheus.io/" rel="noopener noreferrer"&gt;Prometheus&lt;/a&gt; is an open source time series database that uses a "pull" model where it reaches out to configured clients (basically plugins) called exporters. Then ingests the data from the exporters via configured interval which is typically 15 seconds.&lt;/p&gt;
&lt;h3&gt;
  
  
  I want to see some users' pull requests, what now?
&lt;/h3&gt;

&lt;p&gt;If you're familiar with prometheus, you can view the github-pr-exporter docs &lt;a href="https://github.com/circa10a/github-pr-exporter" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Maybe you're not familiar with prometheus, follow along for how to get started!&lt;/p&gt;
&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;The exporter is written in &lt;a href="https://golang.org/" rel="noopener noreferrer"&gt;Go&lt;/a&gt; and utilizes a &lt;a href="https://github.com/google/go-github" rel="noopener noreferrer"&gt;GitHub client library&lt;/a&gt; to execute searches using the GitHub search API. The exporter is basically a CLI that takes in configuration options via arguments then runs a web server in a loop that looks for new pull request data periodically.&lt;/p&gt;
&lt;h3&gt;
  
  
  Searches
&lt;/h3&gt;

&lt;p&gt;The exporter uses the search API and runs this search for every user: &lt;code&gt;"type:pr author:&amp;lt;user&amp;gt; created:&amp;gt;=&amp;lt;calculated timeframe&amp;gt;"&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The search is executed once per user instead of a bulk search due to the 256 character limit of the search API, but because the search API has a rate limit of 10 searches per minute for unauthenticated clients, there is a hard 6 second wait between collecting pull request data for each user to avoid said rate limit. This isn't a huge deal since this would process around 1000 users per 90 minutes which completely fine for this kind of data.&lt;/p&gt;
&lt;h3&gt;
  
  
  Configuration Options
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--config&lt;/code&gt; YAML config file with usernames&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--days-ago&lt;/code&gt; How many days prior to the current to get pull requests from. Defaults to &lt;code&gt;90&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--ignore-user-repos&lt;/code&gt; Ignore pull requests that the user made to their own repositories (no cheating!). Defaults to &lt;code&gt;false&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--interval&lt;/code&gt; How long (in seconds) to sleep between checking for new data. Defaults to &lt;code&gt;21600&lt;/code&gt; (6 hours)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--port&lt;/code&gt; Which port to run the web server on. Defaults to &lt;code&gt;8080&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Deploying github-pr-exporter, prometheus, and grafana
&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%2Fw5h1127mckkb6nz1t2zv.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%2Fw5h1127mckkb6nz1t2zv.png" alt="Alt Text" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are 3 components needed to get up and running to make use of the data.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/circa10a/github-pr-exporter" rel="noopener noreferrer"&gt;github-pr-exporter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://prometheus.io/" rel="noopener noreferrer"&gt;Prometheus&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://grafana.com/" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You will also need docker, docker-compose, and git installed. This will run the 3 components above in the form of containers and connect them together via a docker network.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Start by cloning the repo and cd'ing into the directory
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/circa10a/github-pr-exporter.git
&lt;span class="nb"&gt;cd &lt;/span&gt;github-pr-exporter
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Start the containers
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Profit!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should now be able to access grafana at &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; and the preconfigured dashboard at &lt;a href="http://localhost:3000/d/h_PRluMnk/pull-requests?orgId=1" rel="noopener noreferrer"&gt;http://localhost:3000/d/h_PRluMnk/pull-requests?orgId=1&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The default admin login is username: &lt;code&gt;admin&lt;/code&gt; password: &lt;code&gt;admin&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Monitoring your own user or others
&lt;/h2&gt;

&lt;p&gt;To change the default configuration, simply update &lt;code&gt;examples/config.yaml&lt;/code&gt; to include your GitHub username or others then recreate the containers by running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker-compose down
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then check out the new details on the dashboard! Please note that it can take around 2 minutes for the dashboard to reflect the new data so just try refreshing or turn on auto-refresh.&lt;/p&gt;

</description>
      <category>github</category>
      <category>hacktoberfest</category>
      <category>devops</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>How to use feature toggles with Terraform</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Sat, 23 Jan 2021 19:11:45 +0000</pubDate>
      <link>https://forem.com/circa10a/how-to-use-feature-toggles-with-terraform-28fi</link>
      <guid>https://forem.com/circa10a/how-to-use-feature-toggles-with-terraform-28fi</guid>
      <description>&lt;h2&gt;
  
  
  Reinventing the wheel for the better
&lt;/h2&gt;

&lt;p&gt;I've seen several articles around this subject including the official &lt;a href="https://www.hashicorp.com/blog/terraform-feature-toggles-blue-green-deployments-canary-test" rel="noopener noreferrer"&gt;Hashicorp doc&lt;/a&gt;, but since there wasn't one on &lt;a href="https://dev.to"&gt;dev.to&lt;/a&gt;, I took the typical  "Well I need to rewrite this whole thing because it sucks" software engineer approach 😄.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are feature toggles?
&lt;/h2&gt;

&lt;p&gt;Feature toggles allow you to either enable or disable software functionality via a boolean value (true/false).&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%2F91d0z3bhmib9agxsl6p2.gif" 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%2F91d0z3bhmib9agxsl6p2.gif" alt="alt text" width="480" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why would I use a feature toggle?
&lt;/h2&gt;

&lt;p&gt;The two biggest reasons in my opinion are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Safety&lt;/li&gt;
&lt;li&gt;Have options&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;By having new functionality toggleable, you can dark launch new features without impacting existing software. I work at a large SaaS company and we promote this practice extensively. By having features that can turned on/off selectively, we rollout experimental features to beta customers without impacting anyone else while using the same codebase. Another element to this is for cost optimization, especially when it comes to infrastructure software like Terraform. Maybe I want to enable load balancing or clustering for customers who pay for it, this way I can offer tiered services and tailor deployments to customers needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diving in
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;This remainder of the article assumes you have prior knowledge of or experience with Terraform&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://github.com/circa10a/terraform-feature-toggle-example" rel="noopener noreferrer"&gt;Here's the example repo we'll be following along with&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's say we have a module to provision nginx web servers on digitalocean, it would be nice to have a toggle to enable/disable load balancing to control costs in certain environments. What would that look like?&lt;/p&gt;

&lt;p&gt;Well, ideally I would like to have a module that has the ability create a load balancer or not like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;module&lt;/span&gt; &lt;span class="s2"&gt;"web_servers"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;source&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"./modules/web_servers"&lt;/span&gt;
  &lt;span class="nx"&gt;instance_count&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_count&lt;/span&gt;
  &lt;span class="nx"&gt;load_balancing_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load_balancing_enabled&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;load_balancing_enabled&lt;/code&gt; flag would be pretty useful to give the consumer options.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to create the toggle
&lt;/h2&gt;

&lt;p&gt;There's a few key components to load balancing, we need multiple servers and we need a load balancer to be aware of all of the servers provisioned.&lt;/p&gt;

&lt;p&gt;Let's create a &lt;code&gt;digitalocean_droplet&lt;/code&gt; resource that has a variable for how many instances to create and a load balancer with all of the instances behind it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean_droplet"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_count&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;"web-${count.index}"&lt;/span&gt;
  &lt;span class="nx"&gt;size&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_size&lt;/span&gt;
  &lt;span class="nx"&gt;image&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;
  &lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_data&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${path.module}/files/cloud-init.yaml"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user_data&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean_loadbalancer"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load_balancing_enabled&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&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;"web-servers-loadbalancer"&lt;/span&gt;
  &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;

  &lt;span class="nx"&gt;forwarding_rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;entry_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;entry_protocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http"&lt;/span&gt;

    &lt;span class="nx"&gt;target_port&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;
    &lt;span class="nx"&gt;target_protocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"http"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;healthcheck&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="mi"&gt;22&lt;/span&gt;
    &lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tcp"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;droplet_ids&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digitalocean_droplet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="p"&gt;[*].&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Count
&lt;/h3&gt;

&lt;p&gt;When we tell the terraform module that we have multiple instances, how does it name them? Terraform supports a cool meta-argument called &lt;a href="https://www.terraform.io/docs/language/meta-arguments/count.html" rel="noopener noreferrer"&gt;count&lt;/a&gt;. A meta-argument is simply a language feature that can be applied to any Terraform resource independent of the provider(DO, AWS, GCP, Azure, etc). The &lt;code&gt;count&lt;/code&gt; meta-argument will create the resources as if it were performing a for loop over an array with the number of &lt;code&gt;count&lt;/code&gt; as the iterator.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean_droplet"&lt;/span&gt; &lt;span class="s2"&gt;"web"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_count&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;"web-${count.index}"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So if &lt;code&gt;instance_count&lt;/code&gt; were &lt;code&gt;2&lt;/code&gt;, 2 resources(servers) would be created and named like so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;web-0&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;web-1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any time the &lt;code&gt;count&lt;/code&gt; meta-argument is supplied, Terraform will store these resources as an array in &lt;a href="https://www.terraform.io/docs/language/state/index.html" rel="noopener noreferrer"&gt;Terraform state&lt;/a&gt; named like so:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;digitalocean_droplet.web[0]&lt;/code&gt; or &lt;code&gt;digitalocean_droplet.web.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;digitalocean_droplet.web[1]&lt;/code&gt; or &lt;code&gt;digitalocean_droplet.web.1&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;This is an important concept&lt;/strong&gt; when it comes to feature toggling in Terraform because if we want to selectively turn things off and on, we need use the &lt;code&gt;count&lt;/code&gt; meta-argument on everything so that we can set it to either &lt;code&gt;1&lt;/code&gt; or &lt;code&gt;0&lt;/code&gt;, e.g. create a thing or not.&lt;/p&gt;

&lt;p&gt;Let's look at the next use of &lt;code&gt;count&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"digitalocean_loadbalancer"&lt;/span&gt; &lt;span class="s2"&gt;"public"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;count&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load_balancing_enabled&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we are using a ternary operator as a &lt;a href="https://www.terraform.io/docs/language/expressions/conditionals.html" rel="noopener noreferrer"&gt;conditional expression in Terraform&lt;/a&gt;. The above code reads as:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;load_balancing_enabled&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;create&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="nf"&gt;doNothing&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;h2&gt;
  
  
  Creating the resources
&lt;/h2&gt;

&lt;p&gt;Let's play with some of these parameters and see how Terraform responds.&lt;/p&gt;

&lt;p&gt;To follow along, you'll need to have a digitalocean account to &lt;a href="https://www.digitalocean.com/docs/apis-clis/api/create-personal-access-token/" rel="noopener noreferrer"&gt;create an API token&lt;/a&gt;.&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;# Set digitalocean token for authentication&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;DIGITALOCEAN_TOKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;XXXXX
&lt;span class="c"&gt;# Clone the repo&lt;/span&gt;
git clone https://github.com/circa10a/terraform-feature-toggle-example.git/
&lt;span class="c"&gt;# Change directory into the repo&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;terraform-feature-toggle-example/
&lt;span class="c"&gt;# Install web_servers module and digitalocean provider&lt;/span&gt;
terraform init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you look at the files in the repo, we have a &lt;a href="https://www.terraform.io/docs/language/values/variables.html#variable-definitions-tfvars-files" rel="noopener noreferrer"&gt;&lt;code&gt;default.auto.tfvars&lt;/code&gt;&lt;/a&gt; file which makes it easy to change configurations.&lt;/p&gt;

&lt;p&gt;Here's the default:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nx"&gt;load_balancing_enabled&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will create 1 droplet and no load balancer. Here's the output of &lt;code&gt;terraform apply&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;❯ terraform apply -auto-approve

module.web_servers.digitalocean_droplet.web[0]: Creating...
module.web_servers.digitalocean_droplet.web[0]: Still creating... [10s elapsed]
module.web_servers.digitalocean_droplet.web[0]: Still creating... [20s elapsed]
module.web_servers.digitalocean_droplet.web[0]: Still creating... [30s elapsed]
module.web_servers.digitalocean_droplet.web[0]: Creation complete after 34s [id=227920620]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

droplet_ips = [
  "167.172.28.220",
]
load_balancer_ip = ""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Outputs
&lt;/h2&gt;

&lt;p&gt;Now we have Terraform making decisions about what to create using our toggle variable. We told our &lt;code&gt;web_servers&lt;/code&gt; module to create &lt;code&gt;1&lt;/code&gt; instance and no load balancer, let's change that and enable load balancing by modifying our &lt;code&gt;default.auto.tfvars&lt;/code&gt; file and have &lt;code&gt;load_balancing_enabled = true&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="nx"&gt;load_balancing_enabled&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And now run &lt;code&gt;terraform apply&lt;/code&gt; again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ terraform apply -auto-approve

module.web_servers.digitalocean_droplet.web[0]: Refreshing state... [id=227920620]
module.web_servers.digitalocean_loadbalancer.public[0]: Creating...
module.web_servers.digitalocean_loadbalancer.public[0]: Still creating... [10s elapsed]
module.web_servers.digitalocean_loadbalancer.public[0]: Still creating... [20s elapsed]
module.web_servers.digitalocean_loadbalancer.public[0]: Still creating... [30s elapsed]
module.web_servers.digitalocean_loadbalancer.public[0]: Still creating... [40s elapsed]
module.web_servers.digitalocean_loadbalancer.public[0]: Still creating... [50s elapsed]
module.web_servers.digitalocean_loadbalancer.public[0]: Still creating... [1m0s elapsed]
module.web_servers.digitalocean_loadbalancer.public[0]: Still creating... [1m10s elapsed]
module.web_servers.digitalocean_loadbalancer.public[0]: Creation complete after 1m19s [id=fc36d0bf-12e5-4d7c-a9c2-06f3859588c5]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Outputs:

droplet_ips = [
  "167.172.28.220",
]
load_balancer_ip = "144.126.248.10"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Awesome! 🎉 🎉 🎉
&lt;/h3&gt;

&lt;p&gt;Our load balancer is now being created because we set &lt;code&gt;load_balancing_enabled&lt;/code&gt; to &lt;code&gt;true&lt;/code&gt;, but wait, &lt;a href="https://www.terraform.io/docs/language/values/outputs.html" rel="noopener noreferrer"&gt;outputs&lt;/a&gt; are different. &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%2Fembjmmh6m7xz6ptcxx6h.gif" 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%2Fembjmmh6m7xz6ptcxx6h.gif" alt="alt text" width="582" height="640"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When &lt;code&gt;load_balancing_enabled&lt;/code&gt; was &lt;code&gt;false&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;Outputs:

droplet_ips = [
  "167.172.28.220",
]
load_balancer_ip = ""
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And when &lt;code&gt;load_balancing_enabled&lt;/code&gt; was &lt;code&gt;true&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;Changes to Outputs:
Outputs:

droplet_ips = [
  "167.172.28.220",
]
load_balancer_ip = "144.126.248.10"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We now have a &lt;code&gt;load_balancer_ip&lt;/code&gt; output. Well that's because of 2 reasons.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1.&lt;/strong&gt; If we look at &lt;code&gt;modules/web_server/outputs.tf&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"droplet_ips"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;digitalocean_droplet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web&lt;/span&gt;&lt;span class="p"&gt;[*].&lt;/span&gt;&lt;span class="nx"&gt;ipv4_address&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"load_balancer_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load_balancing_enabled&lt;/span&gt; &lt;span class="err"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;digitalocean_loadbalancer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;public&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="nx"&gt;ip&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our module is conditionally outputting the load balancer's ip based on &lt;code&gt;var.load_balancing_enabled&lt;/code&gt; variable. If it's &lt;code&gt;true&lt;/code&gt;, give the value of &lt;code&gt;digitalocean_loadbalancer.public[0].ip&lt;/code&gt; else &lt;code&gt;""&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Back to count
&lt;/h2&gt;

&lt;p&gt;So why does the output resource value have that &lt;code&gt;0&lt;/code&gt;? If you recall our overview of the &lt;code&gt;count&lt;/code&gt; meta-argument, any resource that has &lt;code&gt;count&lt;/code&gt; set will output its resources as an array, so in this case we're forced to use &lt;code&gt;0&lt;/code&gt; to reference the first (and only) &lt;code&gt;ip&lt;/code&gt; attribute from the &lt;code&gt;digitalocean_loadbalancer.public&lt;/code&gt; resource.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.&lt;/strong&gt; Our primary &lt;code&gt;outputs.tf&lt;/code&gt; in the root of the project, outputs the values above from the module like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"droplet_ips"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web_servers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;droplet_ips&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"load_balancer_ip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;web_servers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;load_balancer_ip&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Recap
&lt;/h2&gt;

&lt;p&gt;Thanks to &lt;code&gt;count&lt;/code&gt; and ternary operators in Terraform, we can make module configuration in Terraform pretty intuitive.&lt;/p&gt;

&lt;p&gt;Let's not forget about all the other added benefits of feature toggling in Terraform::&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decouple deploy from release&lt;/li&gt;
&lt;li&gt;Enable customization for operators as well as consumers&lt;/li&gt;
&lt;li&gt;Save costs on resources you may or may not need&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Don't forget to run &lt;code&gt;terraform destroy&lt;/code&gt; to remove the resources we created! Otherwise you'll see the costs on your bill!&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/circa10a/terraform-feature-toggle-example" rel="noopener noreferrer"&gt;GitHub Repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Additional Resources
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://spacelift.io/" rel="noopener noreferrer"&gt;Spacelift&lt;/a&gt; has an excellent article for more Terraform language features and their usage. Check out &lt;a href="https://spacelift.io/blog/terraform-functions-expressions-loops" rel="noopener noreferrer"&gt;Terraform Functions, Expressions, and Loops&lt;/a&gt; by Spacelift!&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>cloud</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Notifications for free developer swag 🎉</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Mon, 18 Jan 2021 01:27:37 +0000</pubDate>
      <link>https://forem.com/circa10a/notifications-for-free-developer-swag-45eb</link>
      <guid>https://forem.com/circa10a/notifications-for-free-developer-swag-45eb</guid>
      <description>&lt;h2&gt;
  
  
  Backstory
&lt;/h2&gt;

&lt;p&gt;I love building things and recently placed as a runner up in the &lt;a href="https://dev.to/devteam/digitalocean-app-platform-hackathon-winners-announced-ig0"&gt;digitalocean hackathon&lt;/a&gt; and am pretty stoked to be getting &lt;strong&gt;moar hoodies&lt;/strong&gt;! After my subtle brag about getting all the swag, a friend of mine then told me "why don't you make an app to automate getting more hoodies?".&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%2Fgoajb7tx15uaatqicpzo.gif" 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%2Fgoajb7tx15uaatqicpzo.gif" alt="alt text" width="450" height="359"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What a &lt;em&gt;fantastic&lt;/em&gt; idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;I originally had a reddit bot written in node.js and thought "I have so many uses for bots, I don't want to deploy a new one for each idea. That would be a pain."&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%2Fizjou74y9r9g8qavenrb.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%2Fizjou74y9r9g8qavenrb.jpg" alt="alt text" width="733" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For my use case, I went against the whole microservices trend and kept all the scheduling and configuration for my three bots inside one long running process called &lt;code&gt;all-the-things-bot&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For the sake of staying on topic, you can browse the source &lt;a href="https://github.com/circa10a/all-the-things-bot" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Free Dev Shit Bot
&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%2Fi.imgur.com%2F12TpTP6.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%2Fi.imgur.com%2F12TpTP6.png" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A bot that notifies you of potential opportunities for free dev shit also called "swag". Because we always need more laptop stickers, shirts, and hoodies!&lt;/p&gt;

&lt;p&gt;After refactoring my app to support multiple bots, schedules, and configs. I implemented the "Free Dev Shit Bot". The bot is available on two platforms at the time of this writing.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://twitter.com/FreeDevShitBot" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discord.com/oauth2/authorize?scope=bot&amp;amp;client_id=800447160340447322" rel="noopener noreferrer"&gt;Discord&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Twitter
&lt;/h3&gt;

&lt;p&gt;Simply &lt;a href="](https://twitter.com/FreeDevShitBot)"&gt;follow the bot&lt;/a&gt; for updates!&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%2Fi.imgur.com%2FFOYDULy.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%2Fi.imgur.com%2FFOYDULy.png" width="800" height="1196"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Discord
&lt;/h3&gt;

&lt;p&gt;Create a channel called &lt;code&gt;swag&lt;/code&gt; in your discord server and &lt;a href="https://discord.com/oauth2/authorize?scope=bot&amp;amp;client_id=800447160340447322" rel="noopener noreferrer"&gt;add the bot to your server&lt;/a&gt;. The bot will check daily for free swag opportunities and notify you in the channel!&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%2Fi.imgur.com%2FSYMoTfQ.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%2Fi.imgur.com%2FSYMoTfQ.png" width="800" height="561"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What it does
&lt;/h3&gt;

&lt;p&gt;The bot will scan &lt;a href="https://dev.to"&gt;dev.to&lt;/a&gt; for the terms "free swag" and will tweet the article with a link and post to &lt;code&gt;swag&lt;/code&gt; channels on all subscribed discord servers.&lt;/p&gt;

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

&lt;p&gt;At the start of the application, all bots are initialized and scheduled with &lt;a href="https://github.com/node-schedule/node-schedule" rel="noopener noreferrer"&gt;node-schedule&lt;/a&gt;. The Free Dev Shit bot is scheduled to post to twitter and discord servers at 11:55 PM CST. The logic behind the scenes searches the &lt;a href="https://dev.to/search/feed_content?per_page=60&amp;amp;page=0&amp;amp;sort_by=published_at&amp;amp;sort_direction=desc&amp;amp;class_name=Article&amp;amp;search_fields=free+swag"&gt;dev.to &lt;code&gt;feed_content&lt;/code&gt; API&lt;/a&gt; by filtering the last 60 articles which is ordered &lt;em&gt;newest&lt;/em&gt;. The bot then processes those 60 and finds any that are less than 24 hours old and has more than 1 public reaction(for credibility). If we have any results, simply post to all the things. The dev.to API is definitely the MVP here! 🥇&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://twitter.com/FreeDevShitBot" rel="noopener noreferrer"&gt;Follow FreeDevShitBot Twitter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://discord.com/oauth2/authorize?scope=bot&amp;amp;client_id=800447160340447322" rel="noopener noreferrer"&gt;Add FreeDevShitBot to your Discord server&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/circa10a/all-the-things-bot" rel="noopener noreferrer"&gt;My all-the-things-bot source code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>twitter</category>
      <category>javascript</category>
      <category>showdev</category>
      <category>node</category>
    </item>
    <item>
      <title>Monitoring the digitalocean app platform with ...the digitalocean app platform?</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Sat, 09 Jan 2021 18:47:56 +0000</pubDate>
      <link>https://forem.com/circa10a/monitoring-the-digitalocean-app-platform-with-the-digitalocean-app-platform-4971</link>
      <guid>https://forem.com/circa10a/monitoring-the-digitalocean-app-platform-with-the-digitalocean-app-platform-4971</guid>
      <description>&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;I created a pull request to add existing functionality to a popular digitalocean monitoring app to monitor resources on the digitalocean app platform and tied it into my first hackathon entry. Along with adding functionality to monitor the app platform, I added a deploy template + button to allow simple deployment for consumers.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Built for Business&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I contemplated whether or not this should be submitted for the Random Roulette category or not. While this app is not suited to turn a profit, I think it can really help commercial entities monitor and track their infrastructure on digitalocean to monitor costs, availability, and just get general insight into their resources on the cloud platform such as deployments and builds. &lt;/p&gt;

&lt;h3&gt;
  
  
  App Link
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://digitalocean-exporter-gm83a.ondigitalocean.app/metrics" rel="noopener noreferrer"&gt;DigitalOcean Exporter Deployment&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://mcbroken-dashboard-t7vfw.ondigitalocean.app/grafana/d/KF7IjE-Gk/digitalocean-apps?orgId=1" rel="noopener noreferrer"&gt;DigitalOcean App Platform Monitoring Dashboard&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Screenshots
&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%2Fi%2Fqf8655dyappovqomjj7w.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%2Fqf8655dyappovqomjj7w.png" alt="Screen Shot 2021-01-09 at 12.14.16 PM" width="800" height="437"&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%2Fi%2Fssa32a359v3vtnbxn2vf.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%2Fssa32a359v3vtnbxn2vf.png" alt="Screen Shot 2021-01-09 at 12.19.28 PM" width="800" height="435"&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%2Fzwhi33i5fcvskdgb3zq8.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%2Fzwhi33i5fcvskdgb3zq8.png" alt="alt text" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Description
&lt;/h3&gt;

&lt;p&gt;This is actually my second hackathon entry which builds off of &lt;a href="https://dev.to/circa10a/the-mcbroken-dashboard-1eij"&gt;my first app&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For this entry, I went back to our &lt;a href="https://hacktoberfest.digitalocean.com/" rel="noopener noreferrer"&gt;hacktoberfest&lt;/a&gt; roots and took an open source approach. For this entry, &lt;a href="https://github.com/metalmatze/digitalocean_exporter/pull/16" rel="noopener noreferrer"&gt;I made a pull request to add additional features&lt;/a&gt; to the &lt;a href="https://github.com/metalmatze/digitalocean_exporter" rel="noopener noreferrer"&gt;digitalocean prometheus exporter&lt;/a&gt;. This allows any user to deploy the prometheus exporter on the app platform and monitor the status of their apps for things like builds, deployments, regions and tiers.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/circa10a/digitalocean_exporter/tree/app-platform-circa10a" rel="noopener noreferrer"&gt;My DigitalOcean exporter fork&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/metalmatze/digitalocean_exporter/pull/16" rel="noopener noreferrer"&gt;DigitalOcean Exporter Pull Request&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/circa10a/mcbroken-dashboard" rel="noopener noreferrer"&gt;Prometheus/Grafana deployment code from first entry&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Permissive License
&lt;/h3&gt;

&lt;p&gt;MIT&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I've previously worked on the application and this is my 3rd featured added. Given the new capabilities of the platform, it felt like a no-brainer for this functionality to be enabled for the community.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I built it
&lt;/h3&gt;

&lt;p&gt;I wrote some Go code utilizing digitalocean's godo client to pull details from the app platform and converted them to prometheus metrics via a new collector. I also added a digitalocean application deployment template to make it simple for users to deploy.&lt;/p&gt;

&lt;p&gt;Once the new features were ready, I deployed the exporter on the platform and added the endpoint to my existing prometheus configuration to scrape. Once data was being ingested, I created a &lt;a href="https://mcbroken-dashboard-t7vfw.ondigitalocean.app/grafana/d/KF7IjE-Gk/digitalocean-apps?orgId=1" rel="noopener noreferrer"&gt;new grafana dashboard&lt;/a&gt; to showcase available metrics and ideal usage of the data in a real world setting.&lt;/p&gt;

&lt;p&gt;It was lots of fun to make the app platform data more accessible... while also using the platform!&lt;/p&gt;

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

&lt;p&gt;While the pull request hasn't been merged yet, I'm confident that it will be. I even &lt;a href="https://github.com/metalmatze/digitalocean_exporter/pull/17" rel="noopener noreferrer"&gt;added new functionality to scrape digitalocean managed database info&lt;/a&gt; during the hackathon... because why not.&lt;/p&gt;

</description>
      <category>dohackathon</category>
      <category>monitoring</category>
      <category>hackathon</category>
      <category>digitalocean</category>
    </item>
    <item>
      <title>The mcbroken dashboard - digitalocean hackathon entry</title>
      <dc:creator>Caleb Lemoine</dc:creator>
      <pubDate>Mon, 21 Dec 2020 21:58:51 +0000</pubDate>
      <link>https://forem.com/circa10a/the-mcbroken-dashboard-1eij</link>
      <guid>https://forem.com/circa10a/the-mcbroken-dashboard-1eij</guid>
      <description>&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;I built a digitalocean app template that deploys grafana, prometheus, and a custom prometheus exporter which monitors/displays the availability information of all the broken Mcdonald's ice cream machines in the United States.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Random Roulette&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  App Link
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://mcbroken-dashboard-t7vfw.ondigitalocean.app/grafana/d/TmWLGVxMz/broken-mcdonalds-ice-cream-machines-in-the-us?orgId=1" rel="noopener noreferrer"&gt;Click here to go to the mcbroken dashboard&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Screenshots
&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%2Fi%2Fgrj2p68ztx482js68im8.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%2Fgrj2p68ztx482js68im8.png" alt="Alt Text" width="800" height="1600"&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%2Fi%2Fs8euo9ouo9xi4urhpujj.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%2Fs8euo9ouo9xi4urhpujj.png" alt="Alt Text" width="800" height="438"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Description
&lt;/h3&gt;

&lt;p&gt;The mcbroken dashboard is an app template of 3 applications(listed below) which pre-configure a dashboard that is powered by &lt;a href="https://mcbroken.com" rel="noopener noreferrer"&gt;mcbroken.com&lt;/a&gt;. Its purpose is to provide availability information of all the broken Mcdonald's ice cream machines in the United States.&lt;/p&gt;

&lt;p&gt;Stats include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current broken percentage of mcdonald's ice cream machines in the US&lt;/li&gt;
&lt;li&gt;City with the most broken machines and it's outage percentage&lt;/li&gt;
&lt;li&gt;Outage percentage of most major US cities&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;&lt;a href="https://github.com/circa10a/mcbroken-dashboard" rel="noopener noreferrer"&gt;https://github.com/circa10a/mcbroken-dashboard&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Permissive License
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;MIT&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I remember seeing mcbroken.com circle the news a little while ago and thought this was absolutely hilarious. What's the point of being an engineer if you can't over engineer things for a good laugh?&lt;/p&gt;

&lt;p&gt;Not only would it be hilarious, but thought it would also be a good learning opportunity to learn how to wire up multiple components for a single application on digitalocean's new platform.&lt;/p&gt;

&lt;h3&gt;
  
  
  How I built it
&lt;/h3&gt;

&lt;p&gt;I utilized digitalocean's new app platform to deploy an application that consists of a pretty cloud native architecture. Wiring up all the components on the platform was much simpler than I thought. I love that it's essentially kubernetes under the hood so all the existing functional knowledge is applicable, yet there's so much more simplicity when it comes to getting your application to "just work".&lt;/p&gt;

</description>
      <category>dohackathon</category>
      <category>monitoring</category>
      <category>showdev</category>
      <category>cloud</category>
    </item>
  </channel>
</rss>
