<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://incpac.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://incpac.github.io/" rel="alternate" type="text/html" /><updated>2026-04-17T08:14:18+12:00</updated><id>https://incpac.github.io/feed.xml</id><title type="html">Building Something Awesome</title><author><name>Thomas Claridge</name></author><entry><title type="html">Go Concurrency Notes</title><link href="https://incpac.github.io/posts/2025-07-12-go-concurrency-notes/" rel="alternate" type="text/html" title="Go Concurrency Notes" /><published>2025-07-12T00:00:00+12:00</published><updated>2025-07-12T00:00:00+12:00</updated><id>https://incpac.github.io/posts/go-concurrency-notes</id><content type="html" xml:base="https://incpac.github.io/posts/2025-07-12-go-concurrency-notes/"><![CDATA[<p>So I was looking through some Go code Claude wrote for me the other day. It handles bulk uploads to S3. Naturally, not
being a Go developer, I had no idea what it was doing. This Go concurrency stuff might as well be written in ancient
Sumerian. But after getting it explained properly, I figured I’d write it down before my brain decides to forget it all
next week.</p>

<h2 id="the-setup">The Setup</h2>

<p>Picture this: you’ve got a function that needs to upload a bunch of files to S3, but you don’t want to be a complete
savage and upload them one at a time. You also don’t want to be an idiot and try to upload 10,000 files simultaneously
and bring down your entire infrastructure. What you need is something in between - a civilized approach that says “let’s
do 10 at a time, and when one finishes, we can start the next.”</p>

<p>This is where Go’s concurrency patterns come in handy, assuming you can actually figure out what they’re doing.</p>

<h2 id="channels---the-magic-mailboxes">Channels - The Magic Mailboxes</h2>

<p>Channels in Go are basically message-passing tubes. Think of them like those pneumatic tubes at old-school banks, except
instead of cash deposits, you’re sending Go structs around.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">resultChan</span> <span class="k">chan</span><span class="o">&lt;-</span> <span class="n">UploadResult</span>  <span class="c">// Send-only channel</span>
<span class="n">semaphore</span> <span class="k">chan</span> <span class="k">struct</span><span class="p">{}</span>         <span class="c">// Bidirectional channel</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">&lt;-</code> arrow tells you which direction messages can flow:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">chan&lt;- Type</code> = send-only, you can put things in, but not take them out</li>
  <li><code class="language-plaintext highlighter-rouge">&lt;-chan Type</code> = receive-only, you can take things out, but not put them in</li>
  <li><code class="language-plaintext highlighter-rouge">chan Type</code> = bidirectional</li>
</ul>

<p>It’s like having one-way streets for your data, which prevents you from accidentally reading from a channel you should
only be writing to. Very civilized.</p>

<h2 id="semaphores---the-bouncer-pattern">Semaphores - The Bouncer Pattern</h2>

<p>Here’s where it gets clever. A semaphore is basically a bouncer with a fixed number of VIP wristbands:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">semaphore</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="k">struct</span><span class="p">{},</span> <span class="m">10</span><span class="p">)</span>    <span class="c">// 10 wristbands available</span>
<span class="n">semaphore</span> <span class="o">&lt;-</span> <span class="k">struct</span><span class="p">{}{}</span>                 <span class="c">// to start work, take a wristband</span>

<span class="c">// do work here</span>

<span class="o">&lt;-</span><span class="n">semaphore</span>                             <span class="c">// when done, return the wristband</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">struct{}{}</code> bit is just an empty struct that takes up zero memory. We don’t care about the data - we just care
about counting how many operations are running.</p>

<p>When the channel buffer is full (all 10 wristbands are taken), any new goroutine trying to send to it will block and
wait. When someone finishes and takes a message out, the next goroutine automatically unblocks and continues. Go’s
runtime handles all this blocking/unblocking automatically, which is pretty awesome.</p>

<h2 id="waitgroups---the-attendance-tracker">WaitGroups - The Attendance Tracker</h2>

<p>WaitGroups are like a teacher doing attendance. You tell it “expect N students to finish their work” and it’ll then wait
until all N report back that they’re done.</p>

<p>This is crucial because if your <code class="language-plaintext highlighter-rouge">main()</code> function ends while goroutines are still running, the program just exits and
kills everything mid-flight.</p>

<h2 id="the-retry-logic">The Retry Logic</h2>

<p>The upload function also has some retry logic with exponential backoff, which is just a fancy way of saying “if
something fails, wait a bit and try again, but wait longer each time.” Which I suppose takes just as long to say.</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">waitTime</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">(</span><span class="m">1</span><span class="o">&lt;&lt;</span><span class="n">attempt</span><span class="p">)</span> <span class="c">// 1, 2, 4, 8 seconds...</span>
</code></pre></div></div>

<p>That <code class="language-plaintext highlighter-rouge">1&lt;&lt;attempt</code> is bit shifting - it doubles the wait each retry. Much more elegant than hardcoding a bunch of sleep
values.</p>

<h2 id="putting-it-all-together">Putting It All Together</h2>

<p>Here’s a complete example that shows how all these pieces work together:</p>

<div class="language-go highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">package</span> <span class="n">main</span>

<span class="k">import</span> <span class="p">(</span>
    <span class="s">"fmt"</span>
    <span class="s">"sync"</span>
    <span class="s">"time"</span>
<span class="p">)</span>

<span class="k">func</span> <span class="n">main</span><span class="p">()</span> <span class="p">{</span>
    <span class="n">files</span> <span class="o">:=</span> <span class="p">[]</span><span class="kt">string</span><span class="p">{</span> <span class="s">"file1.txt"</span><span class="p">,</span> <span class="s">"file2.txt"</span><span class="p">,</span> <span class="s">"file3.txt"</span><span class="p">,</span> <span class="s">"file4.txt"</span><span class="p">,</span> <span class="s">"file5.txt"</span> <span class="p">}</span>

    <span class="c">// create a semaphore that allows a max of 2 concurrent uploads</span>
    <span class="n">semaphore</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="k">struct</span><span class="p">{},</span> <span class="m">2</span><span class="p">)</span>

    <span class="c">// create a waitgroup to track completion</span>
    <span class="k">var</span> <span class="n">wg</span> <span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span>

    <span class="c">// create a channel to collect results</span>
    <span class="n">resultChan</span> <span class="o">:=</span> <span class="nb">make</span><span class="p">(</span><span class="k">chan</span> <span class="kt">string</span><span class="p">,</span> <span class="nb">len</span><span class="p">(</span><span class="n">files</span><span class="p">))</span>

    <span class="c">// start the uploads</span>
    <span class="k">for</span> <span class="n">_</span><span class="p">,</span> <span class="n">file</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">files</span> <span class="p">{</span>
        <span class="n">wg</span><span class="o">.</span><span class="n">Add</span><span class="p">(</span><span class="m">1</span><span class="p">)</span> <span class="c">// expect one more worker to finish</span>
        <span class="k">go</span> <span class="n">uploadFile</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="n">semaphore</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">wg</span><span class="p">,</span> <span class="n">resultChan</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="c">// wait for all uploads to complete</span>
    <span class="n">wg</span><span class="o">.</span><span class="n">Wait</span><span class="p">()</span>
    <span class="nb">close</span><span class="p">(</span><span class="n">resultChan</span><span class="p">)</span>

    <span class="c">// collect and print results</span>
    <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="s">"Upload results:"</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">result</span> <span class="o">:=</span> <span class="k">range</span> <span class="n">resultChan</span> <span class="p">{</span>
        <span class="n">fmt</span><span class="o">.</span><span class="n">Println</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">func</span> <span class="n">uploadFile</span><span class="p">(</span><span class="n">filename</span> <span class="kt">string</span><span class="p">,</span> <span class="n">semaphore</span> <span class="k">chan</span> <span class="k">struct</span><span class="p">{},</span> <span class="n">wg</span> <span class="o">*</span><span class="n">sync</span><span class="o">.</span><span class="n">WaitGroup</span><span class="p">,</span> <span class="n">results</span> <span class="k">chan</span><span class="o">&lt;-</span> <span class="kt">string</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">defer</span> <span class="n">wg</span><span class="o">.</span><span class="n">Done</span><span class="p">()</span> <span class="c">// called when the function exits</span>

    <span class="c">// grab a semaphore slot (blocks if all slots are taken)</span>
    <span class="n">semaphore</span> <span class="o">&lt;-</span> <span class="k">struct</span><span class="p">{}{}</span>
    <span class="k">defer</span> <span class="k">func</span><span class="p">()</span> <span class="p">{</span> <span class="o">&lt;-</span><span class="n">semaphore</span> <span class="p">}()</span> <span class="c">// release the slot when done</span>

    <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Starting upload: %s</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>

    <span class="c">// retry logic with exponential backoff</span>
    <span class="n">maxRetries</span> <span class="o">:=</span> <span class="m">3</span>
    <span class="k">var</span> <span class="n">err</span> <span class="kt">error</span>

    <span class="k">for</span> <span class="n">attempt</span> <span class="o">:=</span> <span class="m">0</span><span class="p">;</span> <span class="n">attempt</span> <span class="o">&lt;=</span> <span class="n">maxRetries</span><span class="p">;</span> <span class="n">attempt</span><span class="o">++</span> <span class="p">{</span>
        <span class="k">if</span> <span class="n">attempt</span> <span class="o">&gt;</span> <span class="m">0</span> <span class="p">{</span>
            <span class="c">// wait before retry with exponential backoff</span>
            <span class="n">waitTime</span> <span class="o">:=</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Duration</span><span class="p">(</span><span class="m">1</span><span class="o">&lt;&lt;</span><span class="n">attempt</span><span class="p">)</span> <span class="c">// 1s, 2s, 4s, 8s...</span>
            <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Retrying upload %s (attempt %d/%d) after %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
                <span class="n">filename</span><span class="p">,</span> <span class="n">attempt</span><span class="o">+</span><span class="m">1</span><span class="p">,</span> <span class="n">maxRetries</span><span class="o">+</span><span class="m">1</span><span class="p">,</span> <span class="n">waitTime</span><span class="p">)</span>
            <span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="n">waitTime</span><span class="p">)</span>
        <span class="p">}</span>

        <span class="c">// simulate upload work with chance of failure</span>
        <span class="n">err</span> <span class="o">=</span> <span class="n">simulateUpload</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="n">attempt</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">err</span> <span class="o">==</span> <span class="no">nil</span> <span class="p">{</span>
            <span class="k">break</span> <span class="c">// success!</span>
        <span class="p">}</span>

        <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Upload failed for %s: %v</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">filename</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="c">// send results back</span>
    <span class="k">if</span> <span class="n">err</span> <span class="o">!=</span> <span class="no">nil</span> <span class="p">{</span>
        <span class="n">results</span> <span class="o">&lt;-</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"✗ Failed %s after %d attempts: %v"</span><span class="p">,</span> <span class="n">filename</span><span class="p">,</span> <span class="n">maxRetries</span><span class="o">+</span><span class="m">1</span><span class="p">,</span> <span class="n">err</span><span class="p">)</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
        <span class="n">results</span> <span class="o">&lt;-</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Sprintf</span><span class="p">(</span><span class="s">"✓ Uploaded %s"</span><span class="p">,</span> <span class="n">filename</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="n">fmt</span><span class="o">.</span><span class="n">Printf</span><span class="p">(</span><span class="s">"Finished upload: %s (success: %v)</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">filename</span><span class="p">,</span> <span class="n">err</span> <span class="o">==</span> <span class="no">nil</span><span class="p">)</span>
<span class="p">}</span>

<span class="c">// simulateUpload simulates an upload that might fail</span>
<span class="k">func</span> <span class="n">simulateUpload</span><span class="p">(</span><span class="n">filename</span> <span class="kt">string</span><span class="p">,</span> <span class="n">attempt</span> <span class="kt">int</span><span class="p">)</span> <span class="kt">error</span> <span class="p">{</span>
    <span class="c">// simulate work</span>
    <span class="n">time</span><span class="o">.</span><span class="n">Sleep</span><span class="p">(</span><span class="m">1</span> <span class="o">*</span> <span class="n">time</span><span class="o">.</span><span class="n">Second</span><span class="p">)</span>

    <span class="c">// simulate failure on first attempt for some files</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">filename</span> <span class="o">==</span> <span class="s">"file2.txt"</span> <span class="o">||</span> <span class="n">filename</span> <span class="o">==</span> <span class="s">"file4.txt"</span><span class="p">)</span> <span class="o">&amp;&amp;</span> <span class="n">attempt</span> <span class="o">&lt;</span> <span class="m">2</span> <span class="p">{</span>
        <span class="k">return</span> <span class="n">fmt</span><span class="o">.</span><span class="n">Errorf</span><span class="p">(</span><span class="s">"network timeout"</span><span class="p">)</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="no">nil</span> <span class="c">// success</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If you run this, you should see that only two files upload at a time due to the semaphore buffer, but all five will
eventually complete before the program exits.</p>

<h2 id="why-this-pattern-works">Why This Pattern Works</h2>

<p>The whole setup gives you:</p>

<ul>
  <li><strong>Controlled concurrency:</strong> Only N operations at once</li>
  <li><strong>Automatic queuing:</strong> Extra work waits in line</li>
  <li><strong>Graceful handling:</strong> No errors when you hit limits, just blocking</li>
  <li><strong>Clean coordination:</strong> Everything waits for everything else to finish</li>
</ul>

<p>It’s actually quite elegant once you wrap your head around it. Much better than the Python equivalent where you’d
probably end up with a ThreadPoolExecutor and a bunch of boilerplate.</p>

<h2 id="final-thoughts">Final Thoughts</h2>

<p>Go’s concurrency primitives are pretty well designed for this sort of thing. The channel-based approach feels weird
coming from other languages, but it does force you to think about data flow and coordination in a more explicit way.</p>

<p>Now I just need to remember all this the next time I’m staring at Go code wondering why there are arrows pointing at
random struct definitions.</p>

<p>The key takeaway is: channels are mailboxes, semaphores are bouncers with limited wristbands, and WaitGroups are
attendance trackers. Everything else is just implementation details.</p>

<p>Future me will thank me for writing this down.</p>

<p>Probably.</p>]]></content><author><name>Thomas Claridge</name></author><summary type="html"><![CDATA[So I was looking through some Go code Claude wrote for me the other day. It handles bulk uploads to S3. Naturally, not being a Go developer, I had no idea what it was doing. This Go concurrency stuff might as well be written in ancient Sumerian. But after getting it explained properly, I figured I’d write it down before my brain decides to forget it all next week.]]></summary></entry><entry><title type="html">Creating Custom VPC Endpoints with AWS PrivateLink</title><link href="https://incpac.github.io/posts/2025-01-28-creating-vpc-endpoints-with-privatelink/" rel="alternate" type="text/html" title="Creating Custom VPC Endpoints with AWS PrivateLink" /><published>2025-01-28T00:00:00+13:00</published><updated>2025-01-28T00:00:00+13:00</updated><id>https://incpac.github.io/posts/creating-vpc-endpoints-with-privatelink</id><content type="html" xml:base="https://incpac.github.io/posts/2025-01-28-creating-vpc-endpoints-with-privatelink/"><![CDATA[<p>Story Time: So you’re sitting there one day, feet up, lazily watching the monitoring dashboard for the service you’ve
just launched. The boss barges in, “No one wants to buy our service unless they can get it directly in their VPC.” Piss
on it. You put your feet down, sit up straight, and start working on setting up
<a href="https://aws.amazon.com/privatelink/">AWS PrivateLink.</a></p>

<h2 id="what-is-privatelink">What is PrivateLink?</h2>

<blockquote>
  <p>AWS PrivateLink is a networking service that enables you to securely expose your applications or services to other
VPCs or on-premises networks, without requiring them to traverse the public internet or use VPN connections. With
PrivateLink, you can create private endpoints in a consumer VPC that connect directly to services in a service
provider VPC, using private IP addresses. This allows you to keep all traffic between the consumer and provider within
the AWS network, improving security and reducing latency.</p>

  <p><cite>Claude 3 Opus</cite></p>
</blockquote>

<h2 id="ok-but-why-do-we-want-it">OK, But Why Do We Want It?</h2>

<p>There are several reasons to make your service available via PrivateLink such as:</p>

<ul>
  <li>Security: By keeping traffic within the AWS network and avoiding the public internet, you reduce the risk of data
breaches or unauthorized access to your service. Consumers can access your service without needing to open up their
firewall or whitelist your public IPs.</li>
  <li>Simplified Networking: PrivateLink eliminates the need for the likes of VPNs or VPC Peering. Consumers can easily
create an endpoint in their VPC and start using your service, without any additional setup.</li>
  <li>Improved Performance: Because traffic stays within the AWS network, PrivateLink connections offer lower latency and
more consistent performance compared to connections over the public internet.</li>
</ul>

<h2 id="game-plan">Game Plan</h2>

<p>This is what we’re going to build.</p>

<p><img src="/assets/posts/creating-vpc-endpoints-with-privatelink/vpc_diagram.png" alt="VPC Diagram" /></p>

<p>We create a VPC Endpoint Service that users can then create Endpoints from in their own VPCs. The Endpoint Service
points to a Network Load Balancer. We point this NLB to an Application Load Balancer that handles our SSL encryption.
From there, the ALB forwards the request to an EC2 instance running our application.</p>

<p>Using this we can create a VPC Endpoint in our consumer VPC. The EC2 instance in the consumer VPC can access the service
running on the app servers running in the service VPC via the endpoint without needin to go out over the internet.</p>

<h2 id="base-terraform">Base Terraform</h2>

<p>The first thing we’re going to need is some base Terraform configuration.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">terraform</span> <span class="p">{</span>
  <span class="nx">required_version</span> <span class="p">=</span> <span class="s2">"~&gt; 1.5"</span>

  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">aws</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/aws"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"~&gt; 5.39"</span>
    <span class="p">}</span>

    <span class="nx">tls</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/tls"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"~&gt; 4.0"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We’ll need the AWS provider, and because this is only a demo and we’re too lazy to get a signed certificate, we’ll make
use of the TLS provider.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_availability_zones"</span> <span class="s2">"available"</span> <span class="p">{</span>
  <span class="nx">state</span> <span class="p">=</span> <span class="s2">"available"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We want a list of AZs in our region for later use.</p>

<h2 id="creating-the-service">Creating the Service</h2>

<p>Our service is going to be made up of an EC2 instance behind an Application Load Balancer. We also need a Network Load
Balancer for the VPC Endpoint interface.</p>

<h4 id="network">Network</h4>

<p>The service sits in its own VPC. We’re going to create a subnet for each AZ.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">cidr_block</span> <span class="p">=</span> <span class="s2">"10.0.0.0/16"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_subnet"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="p">=</span> <span class="nx">length</span><span class="p">(</span><span class="k">data</span><span class="p">.</span><span class="nx">aws_availability_zones</span><span class="p">.</span><span class="nx">available</span><span class="p">.</span><span class="nx">names</span><span class="p">)</span>

  <span class="nx">vpc_id</span>                  <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">cidr_block</span>              <span class="p">=</span> <span class="nx">cidrsubnet</span><span class="p">(</span><span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">cidr_block</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="nx">count</span><span class="p">.</span><span class="nx">index</span><span class="p">)</span>
  <span class="nx">availability_zone</span>       <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_availability_zones</span><span class="p">.</span><span class="nx">available</span><span class="p">.</span><span class="nx">names</span><span class="p">[</span><span class="nx">count</span><span class="p">.</span><span class="nx">index</span><span class="p">]</span>
  <span class="nx">map_public_ip_on_launch</span> <span class="p">=</span> <span class="kc">true</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We also want a Internet Gateway</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_internet_gateway"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">vpc_id</span> <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_route_table"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">vpc_id</span> <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">route</span> <span class="p">{</span>
    <span class="nx">cidr_block</span> <span class="p">=</span> <span class="s2">"0.0.0.0/0"</span>
    <span class="nx">gateway_id</span> <span class="p">=</span> <span class="nx">aws_internet_gateway</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_route_table_association"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="p">=</span> <span class="nx">length</span><span class="p">(</span><span class="nx">aws_subnet</span><span class="p">.</span><span class="nx">service</span><span class="p">)</span>

  <span class="nx">subnet_id</span>      <span class="p">=</span> <span class="nx">aws_subnet</span><span class="p">.</span><span class="nx">service</span><span class="p">[</span><span class="nx">count</span><span class="p">.</span><span class="nx">index</span><span class="p">].</span><span class="nx">id</span>
  <span class="nx">route_table_id</span> <span class="p">=</span> <span class="nx">aws_route_table</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We’re going to use a Security Group in multiple places. It needs HTTP/S from inside the VPC, and access out to the
intenet so we can install some software on the EC2 instance running our service.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_security_group"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"service"</span>
  <span class="nx">vpc_id</span> <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_security_group_ingress_rule"</span> <span class="s2">"service_https"</span> <span class="p">{</span>
  <span class="nx">security_group_id</span> <span class="p">=</span> <span class="nx">aws_security_group</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"HTTPS from Service VPC"</span>
  <span class="nx">cidr_ipv4</span>   <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">cidr_block</span>
  <span class="nx">from_port</span>   <span class="p">=</span> <span class="mi">443</span>
  <span class="nx">to_port</span>     <span class="p">=</span> <span class="mi">443</span>
  <span class="nx">ip_protocol</span> <span class="p">=</span> <span class="s2">"tcp"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_security_group_ingress_rule"</span> <span class="s2">"service_http"</span> <span class="p">{</span>
  <span class="nx">security_group_id</span> <span class="p">=</span> <span class="nx">aws_security_group</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"HTTP from Service VPC"</span>
  <span class="nx">cidr_ipv4</span>   <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">cidr_block</span>
  <span class="nx">from_port</span>   <span class="p">=</span> <span class="mi">80</span>
  <span class="nx">to_port</span>     <span class="p">=</span> <span class="mi">80</span>
  <span class="nx">ip_protocol</span> <span class="p">=</span> <span class="s2">"tcp"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_security_group_egress_rule"</span> <span class="s2">"service_all"</span> <span class="p">{</span>
  <span class="nx">security_group_id</span> <span class="p">=</span> <span class="nx">aws_security_group</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Allow all egress"</span>
  <span class="nx">cidr_ipv4</span>   <span class="p">=</span> <span class="s2">"0.0.0.0/0"</span>
  <span class="nx">ip_protocol</span> <span class="p">=</span> <span class="s2">"-1"</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="service-compute">Service Compute</h4>

<p>Speaking of an EC2 instance…</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_ami"</span> <span class="s2">"amazon_linux_2023"</span> <span class="p">{</span>
  <span class="nx">owners</span>      <span class="p">=</span> <span class="p">[</span><span class="s2">"amazon"</span><span class="p">]</span>
  <span class="nx">most_recent</span> <span class="p">=</span> <span class="kc">true</span>

  <span class="nx">filter</span> <span class="p">{</span>
    <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"name"</span>
    <span class="nx">values</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"al2023-ami-2023.*-x86_64"</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">filter</span> <span class="p">{</span>
    <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"architecture"</span>
    <span class="nx">values</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"x86_64"</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">filter</span> <span class="p">{</span>
    <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"virtualization-type"</span>
    <span class="nx">values</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"hvm"</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_instance"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">ami</span>                    <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_ami</span><span class="p">.</span><span class="nx">amazon_linux_2023</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">instance_type</span>          <span class="p">=</span> <span class="s2">"t3.nano"</span>
  <span class="nx">subnet_id</span>              <span class="p">=</span> <span class="nx">aws_subnet</span><span class="p">.</span><span class="nx">service</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span>
  <span class="nx">vpc_security_group_ids</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_security_group</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span>
  <span class="nx">user_data</span>              <span class="p">=</span> <span class="nx">file</span><span class="p">(</span><span class="s2">"</span><span class="k">${</span><span class="nx">path</span><span class="p">.</span><span class="k">module}</span><span class="s2">/userdata.sh"</span><span class="p">)</span>

  <span class="nx">key_name</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">key_name</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">Name</span> <span class="p">=</span> <span class="s2">"PrivateLink Demo Service"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The userdata for the instance will simply install and start Nginx.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/sh</span>

<span class="nb">sudo </span>yum <span class="nb">install</span> <span class="nt">-y</span> nginx.x86_64
<span class="nb">sudo </span>systemctl start nginx
</code></pre></div></div>

<h4 id="application-load-balancer">Application Load Balancer</h4>

<p>With our instance running, we can create an Application Load Balancer to route traffic to it.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_lb"</span> <span class="s2">"service_alb"</span> <span class="p">{</span>
  <span class="nx">name</span>               <span class="p">=</span> <span class="s2">"service-alb"</span>
  <span class="nx">internal</span>           <span class="p">=</span> <span class="kc">true</span>
  <span class="nx">load_balancer_type</span> <span class="p">=</span> <span class="s2">"application"</span>
  <span class="nx">security_groups</span>    <span class="p">=</span> <span class="p">[</span><span class="nx">aws_security_group</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span>
  <span class="nx">subnets</span>            <span class="p">=</span> <span class="nx">aws_subnet</span><span class="p">.</span><span class="nx">service</span><span class="p">[*].</span><span class="nx">id</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We need to create a Target Group containing our EC2 instance.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_lb_target_group"</span> <span class="s2">"service_alb"</span> <span class="p">{</span>
  <span class="nx">name</span>     <span class="p">=</span> <span class="s2">"service"</span>
  <span class="nx">port</span>     <span class="p">=</span> <span class="mi">80</span>
  <span class="nx">protocol</span> <span class="p">=</span> <span class="s2">"HTTP"</span>
  <span class="nx">vpc_id</span>   <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_lb_target_group_attachment"</span> <span class="s2">"service_alb"</span> <span class="p">{</span>
  <span class="nx">target_group_arn</span> <span class="p">=</span> <span class="nx">aws_lb_target_group</span><span class="p">.</span><span class="nx">service_alb</span><span class="p">.</span><span class="nx">arn</span>
  <span class="nx">target_id</span>        <span class="p">=</span> <span class="nx">aws_instance</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">port</span>             <span class="p">=</span> <span class="mi">80</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The Listener for the ALB needs a certificate if we want to use HTTPS. We’re going to use the TLS Terraform provider for
this, but generatlly you’d want a cetificate signed by a trusted CA.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"tls_private_key"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">algorithm</span> <span class="p">=</span> <span class="s2">"RSA"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"tls_self_signed_cert"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">private_key_pem</span> <span class="p">=</span> <span class="nx">tls_private_key</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">private_key_pem</span>

  <span class="nx">subject</span> <span class="p">{</span>
    <span class="nx">common_name</span>  <span class="p">=</span> <span class="nx">aws_lb</span><span class="p">.</span><span class="nx">service_alb</span><span class="p">.</span><span class="nx">dns_name</span>
    <span class="nx">organization</span> <span class="p">=</span> <span class="s2">"ACME Demos"</span>
  <span class="p">}</span>

  <span class="nx">validity_period_hours</span> <span class="p">=</span> <span class="mi">72</span> <span class="c1"># 3 days</span>

  <span class="nx">allowed_uses</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s2">"key_encipherment"</span><span class="p">,</span>
    <span class="s2">"digital_signature"</span><span class="p">,</span>
    <span class="s2">"server_auth"</span><span class="p">,</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_acm_certificate"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">private_key</span>      <span class="p">=</span> <span class="nx">tls_private_key</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">private_key_pem</span>
  <span class="nx">certificate_body</span> <span class="p">=</span> <span class="nx">tls_self_signed_cert</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">cert_pem</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Finally we can add our listener to the Load Balancer.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_lb_listener"</span> <span class="s2">"service_alb"</span> <span class="p">{</span>
  <span class="nx">load_balancer_arn</span> <span class="p">=</span> <span class="nx">aws_lb</span><span class="p">.</span><span class="nx">service_alb</span><span class="p">.</span><span class="nx">arn</span>
  <span class="nx">port</span>              <span class="p">=</span> <span class="s2">"443"</span>
  <span class="nx">protocol</span>          <span class="p">=</span> <span class="s2">"HTTPS"</span>
  <span class="nx">ssl_policy</span>        <span class="p">=</span> <span class="s2">"ELBSecurityPolicy-2016-08"</span>
  <span class="nx">certificate_arn</span>   <span class="p">=</span> <span class="nx">aws_acm_certificate</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">arn</span>

  <span class="nx">default_action</span> <span class="p">{</span>
    <span class="nx">type</span>             <span class="p">=</span> <span class="s2">"forward"</span>
    <span class="nx">target_group_arn</span> <span class="p">=</span> <span class="nx">aws_lb_target_group</span><span class="p">.</span><span class="nx">service_alb</span><span class="p">.</span><span class="nx">arn</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="network-load-balancer">Network Load Balancer</h4>

<p>Now that we have our Application Load Balancer set up and pointing to the EC2 instace, we can put the Network Load
Balancer in front of it.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_lb"</span> <span class="s2">"service_nlb"</span> <span class="p">{</span>
  <span class="nx">name</span>                             <span class="p">=</span> <span class="s2">"service-nlb"</span>
  <span class="nx">load_balancer_type</span>               <span class="p">=</span> <span class="s2">"network"</span>
  <span class="nx">enable_cross_zone_load_balancing</span> <span class="p">=</span> <span class="kc">false</span>
  <span class="nx">internal</span>                         <span class="p">=</span> <span class="kc">true</span>
  <span class="nx">subnets</span>                          <span class="p">=</span> <span class="nx">aws_subnet</span><span class="p">.</span><span class="nx">service</span><span class="p">[*].</span><span class="nx">id</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The Load Balancer needs to target the ALB.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_lb_target_group"</span> <span class="s2">"service_nlb"</span> <span class="p">{</span>
  <span class="nx">name</span>        <span class="p">=</span> <span class="s2">"service-nlb"</span>
  <span class="nx">port</span>        <span class="p">=</span> <span class="s2">"443"</span>
  <span class="nx">protocol</span>    <span class="p">=</span> <span class="s2">"TCP"</span>
  <span class="nx">target_type</span> <span class="p">=</span> <span class="s2">"alb"</span>
  <span class="nx">vpc_id</span>      <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">health_check</span> <span class="p">{</span>
    <span class="nx">matcher</span>  <span class="p">=</span> <span class="s2">"200"</span>
    <span class="nx">path</span>     <span class="p">=</span> <span class="s2">"/"</span>
    <span class="nx">port</span>     <span class="p">=</span> <span class="s2">"443"</span>
    <span class="nx">protocol</span> <span class="p">=</span> <span class="s2">"HTTPS"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_lb_target_group_attachment"</span> <span class="s2">"service_nlb"</span> <span class="p">{</span>
  <span class="nx">port</span>             <span class="p">=</span> <span class="nx">aws_lb_listener</span><span class="p">.</span><span class="nx">service_nlb</span><span class="p">.</span><span class="nx">port</span>
  <span class="nx">target_group_arn</span> <span class="p">=</span> <span class="nx">aws_lb_target_group</span><span class="p">.</span><span class="nx">service_nlb</span><span class="p">.</span><span class="nx">arn</span>
  <span class="nx">target_id</span>        <span class="p">=</span> <span class="nx">aws_lb_listener</span><span class="p">.</span><span class="nx">service_alb</span><span class="p">.</span><span class="nx">load_balancer_arn</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And connecting the listener to the LB.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_lb_listener"</span> <span class="s2">"service_nlb"</span> <span class="p">{</span>
  <span class="nx">load_balancer_arn</span> <span class="p">=</span> <span class="nx">aws_lb</span><span class="p">.</span><span class="nx">service_nlb</span><span class="p">.</span><span class="nx">arn</span>
  <span class="nx">port</span>              <span class="p">=</span> <span class="mi">443</span>
  <span class="nx">protocol</span>          <span class="p">=</span> <span class="s2">"TCP"</span>

  <span class="nx">default_action</span> <span class="p">{</span>
    <span class="nx">type</span>             <span class="p">=</span> <span class="s2">"forward"</span>
    <span class="nx">target_group_arn</span> <span class="p">=</span> <span class="nx">aws_lb_target_group</span><span class="p">.</span><span class="nx">service_nlb</span><span class="p">.</span><span class="nx">arn</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="endpoint-service">Endpoint Service</h4>

<p>The last thing to do is to create the VPC Endpoint Service</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc_endpoint_service"</span> <span class="s2">"service"</span> <span class="p">{</span>
  <span class="nx">acceptance_required</span>        <span class="p">=</span> <span class="kc">false</span>
  <span class="nx">network_load_balancer_arns</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_lb</span><span class="p">.</span><span class="nx">service_nlb</span><span class="p">.</span><span class="nx">arn</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And share it with our target account. This is the current account for now but can be changed as needed.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_caller_identity"</span> <span class="s2">"current"</span> <span class="p">{}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_endpoint_service_allowed_principal"</span> <span class="s2">"allowed_aws_accounts"</span> <span class="p">{</span>
  <span class="nx">vpc_endpoint_service_id</span> <span class="p">=</span> <span class="nx">aws_vpc_endpoint_service</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">principal_arn</span>           <span class="p">=</span> <span class="s2">"arn:aws:iam::</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_caller_identity</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">account_id</span><span class="k">}</span><span class="s2">:root"</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="consuming-the-service">Consuming the Service</h2>

<p>With the service created we can move on to standing up an EC2 instance to consume it.</p>

<h4 id="network-1">Network</h4>

<p>First thing we’re going to want to do is create a network. This is essentially the same as the Service VPC.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc"</span> <span class="s2">"consumer"</span> <span class="p">{</span>
  <span class="nx">cidr_block</span> <span class="p">=</span> <span class="s2">"10.0.0.0/16"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_subnet"</span> <span class="s2">"consumer"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="p">=</span> <span class="nx">length</span><span class="p">(</span><span class="k">data</span><span class="p">.</span><span class="nx">aws_availability_zones</span><span class="p">.</span><span class="nx">available</span><span class="p">.</span><span class="nx">names</span><span class="p">)</span>

  <span class="nx">vpc_id</span>                  <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">cidr_block</span>              <span class="p">=</span> <span class="nx">cidrsubnet</span><span class="p">(</span><span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">cidr_block</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="nx">count</span><span class="p">.</span><span class="nx">index</span><span class="p">)</span>
  <span class="nx">availability_zone</span>       <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_availability_zones</span><span class="p">.</span><span class="nx">available</span><span class="p">.</span><span class="nx">names</span><span class="p">[</span><span class="nx">count</span><span class="p">.</span><span class="nx">index</span><span class="p">]</span>
  <span class="nx">map_public_ip_on_launch</span> <span class="p">=</span> <span class="kc">true</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_internet_gateway"</span> <span class="s2">"consumer"</span> <span class="p">{</span>
  <span class="nx">vpc_id</span> <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_route_table"</span> <span class="s2">"consumer"</span> <span class="p">{</span>
  <span class="nx">vpc_id</span> <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">route</span> <span class="p">{</span>
    <span class="nx">cidr_block</span> <span class="p">=</span> <span class="s2">"0.0.0.0/0"</span>
    <span class="nx">gateway_id</span> <span class="p">=</span> <span class="nx">aws_internet_gateway</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">id</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_route_table_association"</span> <span class="s2">"consumer"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="p">=</span> <span class="nx">length</span><span class="p">(</span><span class="nx">aws_subnet</span><span class="p">.</span><span class="nx">consumer</span><span class="p">)</span>

  <span class="nx">subnet_id</span>      <span class="p">=</span> <span class="nx">aws_subnet</span><span class="p">.</span><span class="nx">consumer</span><span class="p">[</span><span class="nx">count</span><span class="p">.</span><span class="nx">index</span><span class="p">].</span><span class="nx">id</span>
  <span class="nx">route_table_id</span> <span class="p">=</span> <span class="nx">aws_route_table</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="consumer-compute">Consumer Compute</h4>

<p>We need an EC2 instance we can log into. We’ll take the name of a key as a variable.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">variable</span> <span class="s2">"key_name"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Name of the Key Pair to use for the EC2 instances"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_instance"</span> <span class="s2">"consumer"</span> <span class="p">{</span>
  <span class="nx">ami</span>                    <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_ami</span><span class="p">.</span><span class="nx">amazon_linux_2023</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">instance_type</span>          <span class="p">=</span> <span class="s2">"t3.nano"</span>
  <span class="nx">subnet_id</span>              <span class="p">=</span> <span class="nx">aws_subnet</span><span class="p">.</span><span class="nx">consumer</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">id</span>
  <span class="nx">vpc_security_group_ids</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_security_group</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span>

  <span class="nx">key_name</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">key_name</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">Name</span> <span class="p">=</span> <span class="s2">"PrivateLink Demo Consumer"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_security_group"</span> <span class="s2">"consumer"</span> <span class="p">{</span>
  <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"consumer"</span>
  <span class="nx">vpc_id</span> <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_security_group_ingress_rule"</span> <span class="s2">"consumer_ssh"</span> <span class="p">{</span>
  <span class="nx">security_group_id</span> <span class="p">=</span> <span class="nx">aws_security_group</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"SSH from the internet"</span>
  <span class="nx">cidr_ipv4</span>   <span class="p">=</span> <span class="s2">"0.0.0.0/0"</span>
  <span class="nx">from_port</span>   <span class="p">=</span> <span class="mi">22</span>
  <span class="nx">to_port</span>     <span class="p">=</span> <span class="mi">22</span>
  <span class="nx">ip_protocol</span> <span class="p">=</span> <span class="s2">"tcp"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_security_group_egress_rule"</span> <span class="s2">"consumer_all"</span> <span class="p">{</span>
  <span class="nx">security_group_id</span> <span class="p">=</span> <span class="nx">aws_security_group</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Allow all egress"</span>
  <span class="nx">cidr_ipv4</span>   <span class="p">=</span> <span class="s2">"0.0.0.0/0"</span>
  <span class="nx">ip_protocol</span> <span class="p">=</span> <span class="s2">"-1"</span>
<span class="p">}</span>

<span class="k">output</span> <span class="s2">"consumer_public_ip"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Public IP address of the Consumer instance"</span>
  <span class="nx">value</span>       <span class="p">=</span> <span class="nx">aws_instance</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">public_ip</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="privatelink">PrivateLink</h4>

<p>Our VPC Endpoint is going to need a Security Group that allows ingress from the VPC.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_security_group"</span> <span class="s2">"consumer_privatelink"</span> <span class="p">{</span>
  <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"consumer_privatelink"</span>
  <span class="nx">vpc_id</span> <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_security_group_ingress_rule"</span> <span class="s2">"consumer_privatelink_https"</span> <span class="p">{</span>
  <span class="nx">security_group_id</span> <span class="p">=</span> <span class="nx">aws_security_group</span><span class="p">.</span><span class="nx">consumer_privatelink</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"HTTPS from Consumer VPC"</span>
  <span class="nx">cidr_ipv4</span>   <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">cidr_block</span>
  <span class="nx">from_port</span>   <span class="p">=</span> <span class="mi">443</span>
  <span class="nx">to_port</span>     <span class="p">=</span> <span class="mi">443</span>
  <span class="nx">ip_protocol</span> <span class="p">=</span> <span class="s2">"tcp"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_security_group_egress_rule"</span> <span class="s2">"consumer_privatelink_all"</span> <span class="p">{</span>
  <span class="nx">security_group_id</span> <span class="p">=</span> <span class="nx">aws_security_group</span><span class="p">.</span><span class="nx">consumer_privatelink</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Allow all outbound"</span>
  <span class="nx">cidr_ipv4</span>   <span class="p">=</span> <span class="s2">"0.0.0.0/0"</span>
  <span class="nx">ip_protocol</span> <span class="p">=</span> <span class="s2">"-1"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And we can finally create the VPC Endpoint.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc_endpoint"</span> <span class="s2">"consumer_privatelink"</span> <span class="p">{</span>
  <span class="nx">service_name</span>       <span class="p">=</span> <span class="nx">aws_vpc_endpoint_service</span><span class="p">.</span><span class="nx">service</span><span class="p">.</span><span class="nx">service_name</span>
  <span class="nx">security_group_ids</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_security_group</span><span class="p">.</span><span class="nx">consumer_privatelink</span><span class="p">.</span><span class="nx">id</span><span class="p">]</span>
  <span class="nx">subnet_ids</span>         <span class="p">=</span> <span class="nx">aws_subnet</span><span class="p">.</span><span class="nx">consumer</span><span class="p">[*].</span><span class="nx">id</span>
  <span class="nx">vpc_endpoint_type</span>  <span class="p">=</span> <span class="s2">"Interface"</span>
  <span class="nx">vpc_id</span>             <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">consumer</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>

<span class="k">output</span> <span class="s2">"consumer_endpoint_dns_names"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Domain names of the Consumer VPC Endpoint"</span>
  <span class="nx">value</span>       <span class="p">=</span> <span class="p">[</span><span class="nx">for</span> <span class="nx">dns_entry</span> <span class="nx">in</span> <span class="nx">aws_vpc_endpoint</span><span class="p">.</span><span class="nx">consumer_privatelink</span><span class="p">.</span><span class="nx">dns_entry</span> <span class="err">:</span> <span class="s2">"https://</span><span class="k">${</span><span class="nx">dns_entry</span><span class="p">.</span><span class="nx">dns_name</span><span class="k">}</span><span class="s2">"</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="testing">Testing</h2>

<p>With all this deployed you should be good to test it out. Connect to the Consumer EC2 instance using the public IP
address in the Terraform outputs. Once connected, cURL the first VPC Endpoint URL in the <code class="language-plaintext highlighter-rouge">consumer_endpoint_dns_names</code>
output variable. You should see the standard Nginx landing page.</p>

<h2 id="conclusion">Conclusion</h2>

<p>In this post, we walked through the process of creating VPC endpoints with AWS PrivateLink using Terraform. We started
by setting up a service provider EC2 instance inside a VPC behind an Application Load Balancer and Network Load Balancer.
We then created a VPC Endpoint Service and shared it with a consumer VPC, allowing the consumer to securely access the
service over the AWS network.</p>

<p>By using PrivateLink, we were able to improve the security, performance, and operational efficiency of our service.
Consumers can now access the service without exposing it to the public internet or configuring complex network settings.
Traffic stays within the AWS network, reducing latency and increasing compliance.</p>

<p>One important aspect of the setup is the allowed principal configuration, which controls which AWS accounts or IAM users
can create endpoints to connect to the service. Here we’ve manually specified the allowed principal based on the current
AWS account ID. However, in a real-world scenario, you may want to automate this depending on how new consumers are to
be added to the system.</p>

<p>AWS PrivateLink is a useful service for securely sharing resources across VPCs and accounts. It can help achieve
networking goals while maintaining high levels of security and performance. The specific implementation details will
depend on your use case and requirements, but the general principles outlined in this post should provide a good
starting point for understanding and working with PrivateLink.</p>

<p>If you have any issues with the Terraform, you can find a working implementation on my
<a href="https://github.com/incpac/aws-privatelink-demo">GitHub.</a></p>]]></content><author><name>Thomas Claridge</name></author><summary type="html"><![CDATA[Story Time: So you’re sitting there one day, feet up, lazily watching the monitoring dashboard for the service you’ve just launched. The boss barges in, “No one wants to buy our service unless they can get it directly in their VPC.” Piss on it. You put your feet down, sit up straight, and start working on setting up AWS PrivateLink.]]></summary></entry><entry><title type="html">Playing Around with AWS IPAM</title><link href="https://incpac.github.io/posts/2024-04-20-playing-around-with-aws-ipam/" rel="alternate" type="text/html" title="Playing Around with AWS IPAM" /><published>2024-04-20T00:00:00+12:00</published><updated>2024-04-20T00:00:00+12:00</updated><id>https://incpac.github.io/posts/playing-around-with-aws-ipam</id><content type="html" xml:base="https://incpac.github.io/posts/2024-04-20-playing-around-with-aws-ipam/"><![CDATA[<p>Story time. <del>Earlier in the month</del> Last month at this point, I was laying out a VPC with some subnets in Terraform.
I didn’t want to have to manually enter subnets so wanted to create them dynamically from the CIDR assigned to the VPC.
I had forgotten what the <a href="https://developer.hashicorp.com/terraform/language/functions/cidrsubnet"><code class="language-plaintext highlighter-rouge">cidrsubnet</code></a>
Terraform function was called and went Googling for it.</p>

<p>Rather than the Terraform docs for the function showing up as the first result I got a StackOverflow (or something
similar) post of someone asking the same thing. Instead of simply recommending the Terraform function the top response
recommended checking out <a href="https://docs.aws.amazon.com/vpc/latest/ipam/what-it-is-ipam.html">AWS’s IP Address Manager.</a>
IPAM is a feature of the VPC service that lets you plan, track, and monitor IP addresses across your workloads.</p>

<p>This looked cool so I wanted to play around with it a bit.</p>

<h1 id="lab-01---intro-to-ipam">Lab 01 - Intro to IPAM</h1>

<p>Reading through the docs the first thing we’re going to want to do is set up an IPAM pool and a VPC in a single account
and a single region. In this case we’re going to go with ap-southeast-2 as that’s my closest, but you can go where ever
you want. This is what we’re going to set up.</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/05-lab01.png" alt="Lab 01" /></p>

<p>To start with we need some standard Terraform config.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">terraform</span> <span class="p">{</span>
  <span class="nx">required_version</span> <span class="p">=</span> <span class="s2">"~&gt; 1.5"</span>

  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">aws</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/aws"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"~&gt; 5.39"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
  <span class="nx">region</span> <span class="p">=</span> <span class="s2">"ap-southeast-2"</span>

  <span class="nx">default_tags</span> <span class="p">{</span>
    <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">Stack</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We then create our root IPAM. Note that we need to list all regions we want to create pools in along with the region the
IPAM is created in. Currently we’re only using our local region but we’re going to want to do more later on.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"current"</span> <span class="p">{}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_ipam"</span> <span class="s2">"test"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"IPAM Test"</span>

  <span class="s2">"operating_regions"</span> <span class="p">{</span>
    <span class="nx">region_name</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span>
  <span class="p">}</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">Name</span> <span class="p">=</span> <span class="s2">"IPAM Test"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Apply this and we can see the IPAM in the AWS console.</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/01-base-ipam.png" alt="Base IPAM" /></p>

<p>Next up we add an IPAM Pool and a CIDR. This is the pool of IP addresses the IPAM can dish out. We’re going to name
this after the account ID which we can pull from the <code class="language-plaintext highlighter-rouge">aws_caller_identity</code> data block.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_caller_identity"</span> <span class="s2">"main"</span> <span class="p">{}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool"</span> <span class="s2">"account"</span> <span class="p">{</span>
  <span class="nx">description</span>    <span class="p">=</span> <span class="s2">"IPAM Test - Account Pool"</span>
  <span class="nx">address_family</span> <span class="p">=</span> <span class="s2">"ipv4"</span>
  <span class="nx">ipam_scope_id</span>  <span class="p">=</span> <span class="nx">aws_vpc_ipam</span><span class="p">.</span><span class="nx">test</span><span class="p">.</span><span class="nx">private_default_scope_id</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">Name</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_caller_identity</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">account_id</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool_cidr"</span> <span class="s2">"account"</span> <span class="p">{</span>
  <span class="nx">ipam_pool_id</span> <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">account</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">cidr</span>         <span class="p">=</span> <span class="s2">"10.0.0.0/16"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Apply this and you should now see the Private Pool</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/02-private-pool.png" alt="Private Pool" /></p>

<p>But you’ll notice this pool isn’t associated with a region. We’re going to need a region pool if we want to be able to
use it to create a VPC. We’ll create this as a child of the root pool.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool"</span> <span class="s2">"region"</span> <span class="p">{</span>
  <span class="nx">description</span>         <span class="p">=</span> <span class="s2">"IPAM Test - Region Pool - </span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">address_family</span>      <span class="p">=</span> <span class="s2">"ipv4"</span>
  <span class="nx">ipam_scope_id</span>       <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">account</span><span class="p">.</span><span class="nx">ipam_scope_id</span>
  <span class="nx">locale</span>              <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">source_ipam_pool_id</span> <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">account</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">Name</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Delegate this pool a chunk IP addresses from the root pool. We’re going to use a <code class="language-plaintext highlighter-rouge">/20</code>. Then we’ll create a VPC using a
<code class="language-plaintext highlighter-rouge">/24</code> CIDR.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool_cidr"</span> <span class="s2">"region"</span> <span class="p">{</span>
  <span class="nx">ipam_pool_id</span>   <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">region</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">netmask_length</span> <span class="p">=</span> <span class="mi">20</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc"</span> <span class="s2">"vpc"</span> <span class="p">{</span>
  <span class="nx">ipv4_ipam_pool_id</span>   <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">region</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">ipv4_netmask_length</span> <span class="p">=</span> <span class="mi">24</span>
  <span class="nx">depends_on</span>          <span class="p">=</span> <span class="p">[</span><span class="nx">aws_vpc_ipam_pool_cidr</span><span class="p">.</span><span class="nx">region</span><span class="p">]</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Name</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With that, we’ve now got a pool of 4096 addresses assigned to Sydney and a VPC with 256 addresses.</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/03-address-allocation.png" alt="Sydney Pool" /></p>

<h1 id="lab-02---lets-go-multi-region">Lab 02 - Let’s go multi-region</h1>

<p>This time around we’re going to add additional regions. The example we’re building will create a VPC in us-east-1 and
us-west-2.</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/06-lab02.png" alt="Lab 02" /></p>

<p>Rather than define every resource for each region manually we’re going to create a <code class="language-plaintext highlighter-rouge">region</code> module and re-use it. First
thing to do is set up the base TF requirements. We need the provider for the main region and one for the region we’re
going to deploy to.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">terraform</span> <span class="p">{</span>
  <span class="nx">required_version</span> <span class="p">=</span> <span class="s2">"~&gt; 1.5"</span>

  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">aws</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/aws"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"~&gt; 5.39"</span>

      <span class="nx">configuration_aliases</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws</span><span class="p">.</span><span class="nx">primary</span><span class="p">,</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">region</span><span class="p">]</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We need to know the ID of the parent IPAM pool, and the size of the VPC we’re going to create.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">variable</span> <span class="s2">"parent_pool_id"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"ID of the parent IPAM Pool"</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"vpc_cidr_size"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">number</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Size of the VPC CIDR block"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Pull some information from the account and existing resources.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"region"</span> <span class="p">{</span> <span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">region</span> <span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_vpc_ipam_pool"</span> <span class="s2">"parent"</span> <span class="p">{</span>
  <span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">primary</span>
  <span class="nx">ipam_pool_id</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">parent_pool_id</span>
<span class="p">}</span>
</code></pre></div></div>

<p>From here we can create the Pool and CIDR for the region. Note that these are created in the primary region where the
parent IPAM Pool resides. When you open up the IPAM console in the target region you won’t be able to see the pool,
however if you go to manually create a VPC in the region you will see the regional pool as an option when selecting the
<code class="language-plaintext highlighter-rouge">IPAM-allocatied IPv4 CIDR block</code> option.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool"</span> <span class="s2">"region"</span> <span class="p">{</span>
  <span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">primary</span>

  <span class="nx">description</span>         <span class="p">=</span> <span class="s2">"Region Pool - </span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">region</span><span class="p">.</span><span class="nx">name</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">address_family</span>      <span class="p">=</span> <span class="s2">"ipv4"</span>
  <span class="nx">ipam_scope_id</span>       <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">parent</span><span class="p">.</span><span class="nx">ipam_scope_id</span>
  <span class="nx">locale</span>              <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">region</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">source_ipam_pool_id</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">parent</span><span class="p">.</span><span class="nx">ipam_pool_id</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Name</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">region</span><span class="p">.</span><span class="nx">name</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool_cidr"</span> <span class="s2">"region"</span> <span class="p">{</span>
  <span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">primary</span>

  <span class="nx">ipam_pool_id</span>   <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">region</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">netmask_length</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vpc_cidr_size</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then we can create the VPC in the target region</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc"</span> <span class="s2">"vpc"</span> <span class="p">{</span>
  <span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">region</span>
  <span class="nx">ipv4_ipam_pool_id</span>   <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">region</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">ipv4_netmask_length</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vpc_cidr_size</span>
  <span class="nx">depends_on</span>          <span class="p">=</span> <span class="p">[</span><span class="nx">aws_vpc_ipam_pool_cidr</span><span class="p">.</span><span class="nx">region</span><span class="p">]</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Name</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">region</span><span class="p">.</span><span class="nx">name</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Back in the parent terraform remove the <code class="language-plaintext highlighter-rouge">aws_vpc_ipam_pool.region</code>,
<code class="language-plaintext highlighter-rouge">aws_vpc_ipam_pool_cidr.region</code>, and <code class="language-plaintext highlighter-rouge">aws_vpc</code> resources.</p>

<p>We’re going to add the following providers</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
  <span class="nx">region</span> <span class="p">=</span> <span class="s2">"us-west-2"</span>
  <span class="nx">alias</span>  <span class="p">=</span> <span class="s2">"region01"</span>

  <span class="nx">default_tags</span> <span class="p">{</span>
    <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Stack</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
  <span class="nx">region</span> <span class="p">=</span> <span class="s2">"us-east-1"</span>
  <span class="nx">alias</span>  <span class="p">=</span> <span class="s2">"region02"</span>

  <span class="nx">default_tags</span> <span class="p">{</span>
    <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Stack</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"region01"</span> <span class="p">{</span> <span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">region01</span> <span class="p">}</span>
<span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"region02"</span> <span class="p">{</span> <span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">region02</span> <span class="p">}</span>

<span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">operating_regions</span> <span class="p">=</span> <span class="nx">distinct</span><span class="p">([</span>
    <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
    <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">region01</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
    <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">region02</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
  <span class="p">])</span>
<span class="p">}</span>
</code></pre></div></div>

<p>What we’re doing here is building a list of regions we want to create pools in along with the region we’re creating the
IPAM in. We then remove any duplicates.</p>

<p>We do it this way so you change what region <code class="language-plaintext highlighter-rouge">region01</code> and <code class="language-plaintext highlighter-rouge">region02</code> are actually sitting in without having to make
any changes to other parts of the code. They can even be the same region as the root pool. We can then iterate over this
local and update the IPAM to operate in each region.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc_ipam"</span> <span class="s2">"test"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"IPAM Test"</span>

  <span class="nx">dynamic</span> <span class="s2">"operating_regions"</span> <span class="p">{</span>
    <span class="nx">for_each</span> <span class="p">=</span> <span class="kd">local</span><span class="p">.</span><span class="nx">operating_regions</span>
    <span class="nx">content</span> <span class="p">{</span>
      <span class="nx">region_name</span> <span class="p">=</span> <span class="nx">operating_regions</span><span class="p">.</span><span class="nx">value</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">Name</span> <span class="p">=</span> <span class="s2">"IPAM Test"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then We create an instance of the <code class="language-plaintext highlighter-rouge">region</code> module for each region.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="s2">"region01"</span> <span class="p">{</span>
  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"./region"</span>

  <span class="nx">providers</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">aws</span><span class="p">.</span><span class="nx">primary</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">,</span>
    <span class="nx">aws</span><span class="p">.</span><span class="nx">region</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">region01</span><span class="p">,</span>
  <span class="p">}</span>

  <span class="nx">parent_pool_id</span> <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">account</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">vpc_cidr_size</span>  <span class="p">=</span> <span class="mi">24</span>
<span class="p">}</span>

<span class="k">module</span> <span class="s2">"region02"</span> <span class="p">{</span>
  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"./region"</span>

  <span class="nx">providers</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">aws</span><span class="p">.</span><span class="nx">primary</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">,</span>
    <span class="nx">aws</span><span class="p">.</span><span class="nx">region</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">region02</span><span class="p">,</span>
  <span class="p">}</span>

  <span class="nx">parent_pool_id</span> <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">account</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">vpc_cidr_size</span>  <span class="p">=</span> <span class="mi">24</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Give this an apply and you should then be able to see a couple of regional pools.</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/04-regional-pools.png" alt="Regional Pools" /></p>

<p>Note that in this example, the VPC is the same size as the regional pool, taking up the entire allocation.</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/07-regional-pool.png" alt="Regional Pool" /></p>

<h1 id="lab-03---baking-it-into-the-aws-org">Lab 03 - Baking it into the AWS Org</h1>

<p>The next move would be to enable IPAM for the entire AWS Organization (assuming you’re using one). In our example
we’ll have a central Shared Services account, and two Test accounts. We will delegate the IPAM for the organization
to the Shared Services accounts.</p>

<p>To do this first you’re going to want to create these three accounts. Then to delegate access log into your root
account, browse to AWS Organizations &gt; Services. Look for Amazon VPC IP Address Manager. Click on “Enable trusted
access”</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/08-enable-trusted-access.png" alt="Enable Trusted Access" /></p>

<p>Follow this with selecting “Show the option to enable trusted access” and entering “enabled” when asked. Next,
go the IPAM console, Planning, and Organization Settings. Click on Delegate. Enter the account ID for your
Shared Services account and click on Save Changes.</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/09-delegate-ipam-administrator.png" alt="Delegate IPAM Admin" /></p>

<p>Switch to the Shared Services account and you’ll see this.</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/10-delegated-ipam-administrator.png" alt="Delegated IPAM Admin" /></p>

<p>Awesome. So same as the first two labs, we need to set up some Terraform.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">terraform</span> <span class="p">{</span>
  <span class="nx">required_version</span> <span class="p">=</span> <span class="s2">"~&gt; 1.5"</span>

  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">aws</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/aws"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"~&gt; 5.39"</span>
    <span class="p">}</span>

    <span class="nx">random</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/random"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"~&gt; 3.5"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
  <span class="nx">region</span> <span class="p">=</span> <span class="s2">"ap-southeast-2"</span>
  <span class="nx">default_tags</span> <span class="p">{</span>
    <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Stack</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

</code></pre></div></div>

<p>We’re going to set up three spokes. Two in account one across two different regions, and one in account two.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">account_id_1</span> <span class="p">=</span> <span class="s2">"111111111111"</span>
  <span class="nx">account_id_2</span> <span class="p">=</span> <span class="s2">"222222222222"</span>
<span class="p">}</span>

<span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
  <span class="nx">region</span> <span class="p">=</span> <span class="s2">"ap-southeast-2"</span>
  <span class="nx">alias</span>  <span class="p">=</span> <span class="s2">"spoke01"</span>

  <span class="nx">assume_role</span> <span class="p">{</span>
    <span class="nx">role_arn</span> <span class="p">=</span> <span class="s2">"arn:aws:iam::</span><span class="k">${</span><span class="kd">local</span><span class="p">.</span><span class="nx">account_id_1</span><span class="k">}</span><span class="s2">:role/SharedServices-AdministratorAccess"</span>
  <span class="p">}</span>

  <span class="nx">default_tags</span> <span class="p">{</span>
    <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Stack</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
  <span class="nx">region</span> <span class="p">=</span> <span class="s2">"ap-southeast-2"</span>
  <span class="nx">alias</span>  <span class="p">=</span> <span class="s2">"spoke02"</span>

  <span class="nx">assume_role</span> <span class="p">{</span>
    <span class="nx">role_arn</span> <span class="p">=</span> <span class="s2">"arn:aws:iam::</span><span class="k">${</span><span class="kd">local</span><span class="p">.</span><span class="nx">account_id_2</span><span class="k">}</span><span class="s2">:role/SharedServices-AdministratorAccess"</span>
  <span class="p">}</span>

  <span class="nx">default_tags</span> <span class="p">{</span>
    <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Stack</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
  <span class="nx">region</span> <span class="p">=</span> <span class="s2">"us-west-2"</span>
  <span class="nx">alias</span>  <span class="p">=</span> <span class="s2">"spoke03"</span>

  <span class="nx">assume_role</span> <span class="p">{</span>
    <span class="nx">role_arn</span> <span class="p">=</span> <span class="s2">"arn:aws:iam::</span><span class="k">${</span><span class="kd">local</span><span class="p">.</span><span class="nx">account_id_1</span><span class="k">}</span><span class="s2">:role/SharedServices-AdministratorAccess"</span>
  <span class="p">}</span>

  <span class="nx">default_tags</span> <span class="p">{</span>
    <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Stack</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>But before we get to that we need to configure the Organization level settings. This involves creating the IPAM and the
shared pool, creating the Resource Access Manager share and sharing it to the Organization, and creating the root CDIR
block.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">variable</span> <span class="s2">"total_pool_cidr"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"CIDR range for the entire pool"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="s2">"10.0.0.0/8"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_ipam"</span> <span class="s2">"main"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span>

  <span class="nx">dynamic</span> <span class="s2">"operating_regions"</span> <span class="p">{</span>
    <span class="nx">for_each</span> <span class="p">=</span> <span class="kd">local</span><span class="p">.</span><span class="nx">operating_regions</span>
    <span class="nx">content</span> <span class="p">{</span> <span class="nx">region_name</span> <span class="p">=</span> <span class="nx">operating_regions</span><span class="p">.</span><span class="nx">value</span> <span class="p">}</span>
  <span class="p">}</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Name</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool"</span> <span class="s2">"shared"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Org Pool"</span>

  <span class="nx">address_family</span> <span class="p">=</span> <span class="s2">"ipv4"</span>
  <span class="nx">ipam_scope_id</span>  <span class="p">=</span> <span class="nx">aws_vpc_ipam</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">private_default_scope_id</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_ram_resource_share"</span> <span class="s2">"ipam_pool"</span> <span class="p">{</span>
  <span class="nx">name</span>                      <span class="p">=</span> <span class="s2">"IPAM Lab"</span>
  <span class="nx">allow_external_principals</span> <span class="p">=</span> <span class="kc">false</span>
  <span class="nx">permission_arns</span>           <span class="p">=</span> <span class="p">[</span>
    <span class="s2">"arn:aws:ram::aws:permission/AWSRAMDefaultPermissionsIpamPool"</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_ram_resource_association"</span> <span class="s2">"ipam_pool"</span> <span class="p">{</span>
  <span class="nx">resource_arn</span>       <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">shared</span><span class="p">.</span><span class="nx">arn</span>
  <span class="nx">resource_share_arn</span> <span class="p">=</span> <span class="nx">aws_ram_resource_share</span><span class="p">.</span><span class="nx">ipam_pool</span><span class="p">.</span><span class="nx">arn</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_organizations_organization"</span> <span class="s2">"org"</span> <span class="p">{}</span>

<span class="k">resource</span> <span class="s2">"aws_ram_principal_association"</span> <span class="s2">"org"</span> <span class="p">{</span>
  <span class="nx">principal</span>          <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_organizations_organization</span><span class="p">.</span><span class="nx">org</span><span class="p">.</span><span class="nx">arn</span>
  <span class="nx">resource_share_arn</span> <span class="p">=</span> <span class="nx">aws_ram_resource_share</span><span class="p">.</span><span class="nx">ipam_pool</span><span class="p">.</span><span class="nx">arn</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool_cidr"</span> <span class="s2">"shared"</span> <span class="p">{</span>
  <span class="nx">ipam_pool_id</span> <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">shared</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">cidr</span>         <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">total_pool_cidr</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The next step is to create a Pool per region. We do this by creating a list of regions for each spoke, then removing
all duplicates. From there we can iterate over the list. But first we need our Region module. In a directory named
<code class="language-plaintext highlighter-rouge">region</code> create the following.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">terraform</span> <span class="p">{</span>
  <span class="nx">required_version</span> <span class="p">=</span> <span class="s2">"~&gt; 1.5"</span>

  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">aws</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/aws"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"~&gt; 5.39"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"curent"</span> <span class="p">{}</span>

<span class="k">variable</span> <span class="s2">"parent_pool_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"ID of the parent address pool"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"netmask_length"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Length of the Netmask to allocate to this region"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">number</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"region"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Region to create the address pool for"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_vpc_ipam_pool"</span> <span class="s2">"parent"</span> <span class="p">{</span>
  <span class="nx">ipam_pool_id</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">parent_pool_id</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"deploy"</span> <span class="p">{}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool"</span> <span class="s2">"deploy"</span> <span class="p">{</span>
  <span class="nx">description</span>         <span class="p">=</span> <span class="s2">"Region Pool - </span><span class="k">${</span><span class="kd">var</span><span class="p">.</span><span class="nx">region</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">address_family</span>      <span class="p">=</span> <span class="s2">"ipv4"</span>
  <span class="nx">ipam_scope_id</span>       <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">parent</span><span class="p">.</span><span class="nx">ipam_scope_id</span>
  <span class="nx">locale</span>              <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">region</span>
  <span class="nx">source_ipam_pool_id</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">parent</span><span class="p">.</span><span class="nx">ipam_pool_id</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool_cidr"</span> <span class="s2">"deploy"</span> <span class="p">{</span>
  <span class="nx">ipam_pool_id</span>   <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">deploy</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">netmask_length</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">netmask_length</span>
<span class="p">}</span>

<span class="k">output</span> <span class="s2">"ipam_pool_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"ID of the VPC IPAM pool"</span>
  <span class="nx">value</span> <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">deploy</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>
</code></pre></div></div>

<p>What we’re doing here is taking a Parent IPAM pool and a CIDR length and creating a child IPAM pool for the region.</p>

<p>We call this with the following:</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"main"</span> <span class="p">{}</span>
<span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"spoke01"</span> <span class="p">{</span> <span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">spoke01</span> <span class="p">}</span>
<span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"spoke02"</span> <span class="p">{</span> <span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">spoke02</span> <span class="p">}</span>
<span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"spoke03"</span> <span class="p">{</span> <span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">spoke03</span> <span class="p">}</span>

<span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">operating_regions</span> <span class="p">=</span> <span class="nx">distinct</span><span class="p">([</span>
    <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
    <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">spoke01</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
    <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">spoke02</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
    <span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">spoke03</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
  <span class="p">])</span>
<span class="p">}</span>

<span class="k">module</span> <span class="s2">"regions"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="p">=</span> <span class="nx">toset</span><span class="p">(</span><span class="kd">local</span><span class="p">.</span><span class="nx">operating_regions</span><span class="p">)</span>

  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"./region"</span>

  <span class="nx">parent_pool_id</span> <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">shared</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">region</span>         <span class="p">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span>
  <span class="nx">netmask_length</span> <span class="p">=</span> <span class="mi">13</span>
<span class="p">}</span>

<span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">regional_pools</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">for</span> <span class="nx">k</span><span class="p">,</span> <span class="nx">v</span> <span class="nx">in</span> <span class="k">module</span><span class="p">.</span><span class="nx">regions</span> <span class="err">:</span> <span class="nx">k</span> <span class="p">=</span><span class="err">&gt;</span> <span class="nx">v</span><span class="p">.</span><span class="nx">ipam_pool_id</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>As described earlier, what we’re doing here is creating an <code class="language-plaintext highlighter-rouge">aws_region</code> data block for each region. We then use these to
create a list of each region name, removing the duplicates using the <code class="language-plaintext highlighter-rouge">distinct()</code> Terraform function. A Region module
instance is then created for each region in the list. We then create a <code class="language-plaintext highlighter-rouge">regional_pools</code> map linking the name of the
region to the IPAM Pool ID for later reference.</p>

<p><del>Now that we’ve got a pool for each region we’re operating in, we need to create a child pool for each account
operating in each region. First create an <code class="language-plaintext highlighter-rouge">account</code> module.</del></p>

<p><strong>Update:</strong> There used to be a third tier pool here for the account/region combo. It didn’t work and I ripped it out
while troubleshooting and didn’t keep the code (I should probably keep all this stuff in git). Instead we’re placing
the VPC directly in the region pool.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">terraform</span> <span class="p">{</span>
  <span class="nx">required_version</span> <span class="p">=</span> <span class="s2">"~&gt; 1.5"</span>

  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">aws</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/aws"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"~&gt; 5.39"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"vpc_netmask_length"</span> <span class="p">{</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">number</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Netmask length for the default VPC"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="mi">24</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"regional_pools"</span> <span class="p">{</span>
  <span class="nx">type</span> <span class="p">=</span> <span class="nx">map</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Dict of IPAM pool IDs for each region"</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_caller_identity"</span> <span class="s2">"deploy"</span> <span class="p">{}</span>
<span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"deploy"</span> <span class="p">{}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc"</span> <span class="s2">"vpc"</span> <span class="p">{</span>
  <span class="nx">ipv4_ipam_pool_id</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">regional_pools</span><span class="p">[</span><span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">deploy</span><span class="p">.</span><span class="nx">name</span><span class="p">]</span>
  <span class="nx">ipv4_netmask_length</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vpc_netmask_length</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">Name</span> <span class="p">=</span> <span class="s2">"</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_caller_identity</span><span class="p">.</span><span class="nx">deploy</span><span class="p">.</span><span class="nx">account_id</span><span class="k">}</span><span class="s2">/</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">deploy</span><span class="p">.</span><span class="nx">name</span><span class="k">}</span><span class="s2">"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then back in the root we want one instance of this module per spoke provider.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="s2">"spoke01"</span> <span class="p">{</span>
  <span class="nx">depends_on</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_ram_principal_association</span><span class="p">.</span><span class="nx">org</span><span class="p">]</span>

  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"./account"</span>

  <span class="nx">providers</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">aws</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">spoke01</span>
  <span class="p">}</span>

  <span class="nx">regional_pools</span> <span class="p">=</span> <span class="kd">local</span><span class="p">.</span><span class="nx">regional_pools</span>
<span class="p">}</span>

<span class="k">module</span> <span class="s2">"spoke02"</span> <span class="p">{</span>
  <span class="nx">depends_on</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_ram_principal_association</span><span class="p">.</span><span class="nx">org</span><span class="p">]</span>

  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"./account"</span>

  <span class="nx">providers</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">aws</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">spoke02</span>
  <span class="p">}</span>

  <span class="nx">regional_pools</span> <span class="p">=</span> <span class="kd">local</span><span class="p">.</span><span class="nx">regional_pools</span>
<span class="p">}</span>

<span class="k">module</span> <span class="s2">"spoke03"</span> <span class="p">{</span>
  <span class="nx">depends_on</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_ram_principal_association</span><span class="p">.</span><span class="nx">org</span><span class="p">]</span>

  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"./account"</span>

  <span class="nx">providers</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">aws</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">spoke03</span>
  <span class="p">}</span>

  <span class="nx">regional_pools</span> <span class="p">=</span> <span class="kd">local</span><span class="p">.</span><span class="nx">regional_pools</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With this we should be good to apply. It will set up the root IPAM Pool, a sub-pool for each region, then a VPC.</p>

<h2 id="and-cue-the-dramatic-entrance-of-chaos-and-calamity-right-on-schedule">And cue the dramatic entrance of chaos and calamity, right on schedule</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>│ Error: creating EC2 VPC: operation error EC2: CreateVpc, https response error StatusCode: 400, RequestID:
│ 4ca0edc8-06b2-40a3-9297-cf1a80a897a4, api error InvalidIpamPoolId.NotFound: The ipam-pool ID
│ 'ipam-pool-0aaf0f8e9e9010072' does not exist
</code></pre></div></div>

<p>So what I was trying to do here wasn’t going to work. I had thought sharing the root IPAM Pool at the Organization
level would make it available to all accounts. Technically it does, but child pools are considered their own resource.
You would need to share each one out to the Organization.</p>

<p>My original thought was to create a pool per region, then split that up. That way if something went wrong you could
instantly tell where in the world the problem was based on the IP address. Probably a hang up from when I was managing
servers across multiple datacenters where finding them was easier going Region-&gt;Environment vs Environment-&gt;Region.</p>

<p>Though technically we could continue on this way. You would just need to create a RAM share for each regional pool. But
that sounds like a pain in the ass. You could go the other way, create pool per environment then create regional pools
inside it, but then you end up with the same problem of needing to create a share per environment.</p>

<p>No, I think I’m just going to skip the two tiered approach here and create a pool per environment/region.</p>

<h1 id="lab-04---simplifying-things-a-bit">Lab 04 - Simplifying things a bit</h1>

<p>We’re going to build off of Lab 03 (no point letting all that go to waste)</p>

<p>Trash the account and regional stuff. You should be down to just the root IPAM pool and the RAM share.</p>

<h2 id="side-tangent">Side tangent</h2>

<p>I used to be against mono-repos. I like having things split up as required. However a couple of recent projects have me
reconsidering that. Having to coordinate changes across three or more stacks has started to get difficult.</p>

<p>Having said that, I think I might still split this one one. The Org level IPAM is shared out to all accounts in the
AWS Organization. You don’t have to update the share each time you add a new account. This would be my main concern
against splitting it up and it doesn’t apply here.</p>

<p>But if we do split it up, then each account/region becomes their own environment. The only thing that we need to pass in
is the root IPAM ID and that isn’t likely to ever change.</p>

<p>Yeah, I think I like that. However, I’m going to take it in as a variable rather than read it from a remote state.</p>

<h2 id="back-to-it">Back to it</h2>

<p>OK, so we’ve got more to rip out. Get rid of the providers for the spokes. Add a variable for a list of regions to
operate in. We’re then going to create a <code class="language-plaintext highlighter-rouge">local</code> that takes this list, adds the current region to it, and removes any
duplicates.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">variable</span> <span class="s2">"operating_regions"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"List of regions to create the Pool for"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">list</span><span class="p">(</span><span class="nx">string</span><span class="p">)</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="p">[</span><span class="s2">"ap-southeast-2"</span><span class="p">,</span> <span class="s2">"us-west-2"</span><span class="p">]</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"current"</span> <span class="p">{}</span>

<span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">operating_regions</span> <span class="p">=</span> <span class="nx">distinct</span><span class="p">(</span><span class="nx">concat</span><span class="p">([</span><span class="k">data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span><span class="p">],</span> <span class="kd">var</span><span class="p">.</span><span class="nx">operating_regions</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Add an output to the stack for the IPAM ID.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">output</span> <span class="s2">"ipam_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"ID of the VPC IPAM"</span>
  <span class="nx">value</span>       <span class="p">=</span> <span class="nx">aws_vpc_ipam</span><span class="p">.</span><span class="nx">main</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With that done you should be good to apply. The IPAM ID will be spit out at the end. We’re going to need this later.</p>

<p>In another directory we’re going to start from scratch for a spoke.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">terraform</span> <span class="p">{</span>
  <span class="nx">required_version</span> <span class="p">=</span> <span class="s2">"~&gt; 1.5"</span>

  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">aws</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/aws"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"~&gt; 5.39"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
  <span class="nx">region</span> <span class="p">=</span> <span class="s2">"ap-southeast-2"</span>

  <span class="nx">default_tags</span> <span class="p">{</span>
    <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Stack</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"region"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"ID of the AWS region to deploy to"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"iam_role"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"ARN of the IAM role in the target account to assume"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
  <span class="nx">alias</span>  <span class="p">=</span> <span class="s2">"deploy"</span>
  <span class="nx">region</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">region</span>

  <span class="nx">assume_role</span> <span class="p">{</span>
    <span class="nx">role_arn</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">iam_role</span>
  <span class="p">}</span>

  <span class="nx">default_tags</span> <span class="p">{</span>
    <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">Stack</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Next up we want to create the IPAM pool</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">variable</span> <span class="s2">"parent_pool_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"ID of the parent IPAM pool"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"netmask_length"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Length of the Netmask to allocate to this spoke"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">number</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"name"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Name of the spoke"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">string</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="kc">null</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_caller_identity"</span> <span class="s2">"deploy"</span> <span class="p">{</span>
  <span class="k">provider</span> <span class="p">=</span> <span class="nx">aws</span><span class="p">.</span><span class="nx">deploy</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_vpc_ipam_pool"</span> <span class="s2">"parent"</span> <span class="p">{</span>
  <span class="nx">ipam_pool_id</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">parent_pool_id</span>
<span class="p">}</span>

<span class="nx">locals</span> <span class="p">{</span>
  <span class="nx">pool_name</span> <span class="p">=</span> <span class="s2">"</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_caller_identity</span><span class="p">.</span><span class="nx">deploy</span><span class="p">.</span><span class="nx">account_id</span><span class="k">}</span><span class="s2">/</span><span class="k">${</span><span class="kd">var</span><span class="p">.</span><span class="nx">region</span><span class="k">}</span><span class="s2">"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool"</span> <span class="s2">"pool"</span> <span class="p">{</span>
  <span class="nx">description</span>         <span class="p">=</span> <span class="s2">"Spoke Pool - </span><span class="k">${</span><span class="kd">local</span><span class="p">.</span><span class="nx">pool_name</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">address_family</span>      <span class="p">=</span> <span class="s2">"ipv4"</span>
  <span class="nx">ipam_scope_id</span>       <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">parent</span><span class="p">.</span><span class="nx">ipam_scope_id</span>
  <span class="nx">locale</span>              <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">region</span>
  <span class="nx">source_ipam_pool_id</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">parent</span><span class="p">.</span><span class="nx">ipam_pool_id</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We give that an apply to see how we’re going and … damn. Another road block.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>│ Error: creating IPAM Pool: UnauthorizedOperation: You are not authorized to perform this operation. User:
│ arn:aws:sts::1111111111111:assumed-role/SharedServices-AdministratorAccess/aws-go-sdk-1714091794708223000 is not
│ authorized to perform: ec2:CreateIpamPool on resource: arn:aws:ec2::222222222222:ipam-pool/ipam-pool-053687ec45ef90dfc
│ because no resource-based policy allows the ec2:CreateIpamPool action.
</code></pre></div></div>

<p>A quick check of the policy on the RAM share and indeed, remote account cannot create sub-pools. Bugger.</p>

<p>At this point I’m already a bit annoyed we have to tell IPAM which regions we may possible want to operate in in
advance, I really don’t want to have to deal with sharing the sub-pools.</p>

<p>What if we don’t share the root IPAM pool, but create a single sub-pool that uses the entirety of the root and share
that?</p>

<p>…</p>

<p>And it occurs to me that a pool needs to be tied to a region for us to be able to make any real use of it. Which
may explain why I can’t do a lot with the root pool share in the other accounts/regions. This is what I get for taking
so long to write this post.</p>

<p>It’s at this point it occurs to me that we can use a single RAM share and include all regional Pools in it. We don’t
have to create a share for each pool.</p>

<p>Okay back to the root stack we create a pool per region.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool"</span> <span class="s2">"shared"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="p">=</span> <span class="nx">toset</span><span class="p">(</span><span class="kd">local</span><span class="p">.</span><span class="nx">operating_regions</span><span class="p">)</span>

  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Shared Pool - </span><span class="k">${</span><span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="k">}</span><span class="s2">"</span>

  <span class="nx">address_family</span>      <span class="p">=</span> <span class="s2">"ipv4"</span>
  <span class="nx">ipam_scope_id</span>       <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">org</span><span class="p">.</span><span class="nx">ipam_scope_id</span>
  <span class="nx">source_ipam_pool_id</span> <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">org</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">locale</span>              <span class="p">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Update the RAM share for these.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_ram_resource_association"</span> <span class="s2">"ipam_pool"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">shared</span>

  <span class="nx">resource_arn</span>       <span class="p">=</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">.</span><span class="nx">arn</span>
  <span class="nx">resource_share_arn</span> <span class="p">=</span> <span class="nx">aws_ram_resource_share</span><span class="p">.</span><span class="nx">ipam_pool</span><span class="p">.</span><span class="nx">arn</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now we need to rethink subnetting. We have to split up the Org subnet into each region, but we may also want to include
additional regions at a later date. I’m thinking we go for a <code class="language-plaintext highlighter-rouge">/12</code>. This gives us 16 regions or private scopes. If we
want to create <code class="language-plaintext highlighter-rouge">/16</code> VPCs, we can get 16 of them per region.</p>

<p>Back in the org stack</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc_ipam_pool_cidr"</span> <span class="s2">"shared"</span> <span class="p">{</span>
  <span class="nx">for_each</span> <span class="p">=</span> <span class="nx">toset</span><span class="p">(</span><span class="kd">local</span><span class="p">.</span><span class="nx">operating_regions</span><span class="p">)</span>

  <span class="nx">ipam_pool_id</span> <span class="p">=</span> <span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">shared</span><span class="p">[</span><span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">].</span><span class="nx">id</span>
  <span class="nx">cidr</span>         <span class="p">=</span> <span class="nx">cidrsubnet</span><span class="p">(</span><span class="nx">aws_vpc_ipam_pool_cidr</span><span class="p">.</span><span class="nx">org</span><span class="p">.</span><span class="nx">cidr</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="nx">index</span><span class="p">(</span><span class="kd">local</span><span class="p">.</span><span class="nx">operating_regions</span><span class="p">,</span> <span class="nx">each</span><span class="p">.</span><span class="nx">value</span><span class="p">))</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Apply that and you should be able to see our two regional pools</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/11-regional-pools.png" alt="Regional Pools" /></p>

<p>I’ve also over complicated the spokes. Let’s rip all that out and start again.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">terraform</span> <span class="p">{</span>
  <span class="nx">required_version</span> <span class="p">=</span> <span class="s2">"~&gt; 1.5"</span>

  <span class="nx">required_providers</span> <span class="p">{</span>
    <span class="nx">aws</span> <span class="p">=</span> <span class="p">{</span>
      <span class="nx">source</span>  <span class="p">=</span> <span class="s2">"hashicorp/aws"</span>
      <span class="nx">version</span> <span class="p">=</span> <span class="s2">"~&gt; 5.39"</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">provider</span> <span class="s2">"aws"</span> <span class="p">{</span>
  <span class="nx">region</span> <span class="p">=</span> <span class="s2">"ap-southeast-2"</span>

  <span class="nx">default_tags</span> <span class="p">{</span>
    <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span> <span class="nx">Stack</span> <span class="p">=</span> <span class="s2">"IPAM Lab"</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"netmask_length"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Length of the Netmask to allocate to this spoke"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">number</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We can retrieve the IPAM Pool using the name. This way we don’t have to go looking for the right pool for the region
we’re deploying into.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"current"</span> <span class="p">{}</span>

<span class="k">data</span> <span class="s2">"aws_vpc_ipam_pool"</span> <span class="s2">"pool"</span> <span class="p">{</span>
  <span class="nx">filter</span> <span class="p">{</span>
    <span class="nx">name</span>   <span class="p">=</span> <span class="s2">"description"</span>
    <span class="nx">values</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"Shared Pool - </span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span><span class="k">}</span><span class="s2">"</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We can then create a VPC and some subnets</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_vpc"</span> <span class="s2">"vpc"</span> <span class="p">{</span>
  <span class="nx">ipv4_ipam_pool_id</span>   <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_vpc_ipam_pool</span><span class="p">.</span><span class="nx">pool</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">ipv4_netmask_length</span> <span class="p">=</span> <span class="mi">20</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_availability_zones"</span> <span class="s2">"available"</span> <span class="p">{</span>
  <span class="nx">state</span> <span class="p">=</span> <span class="s2">"available"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_subnet"</span> <span class="s2">"public"</span> <span class="p">{</span>
  <span class="nx">count</span> <span class="p">=</span> <span class="nx">length</span><span class="p">(</span><span class="k">data</span><span class="p">.</span><span class="nx">aws_availability_zones</span><span class="p">.</span><span class="nx">available</span><span class="p">.</span><span class="nx">names</span><span class="p">)</span>

  <span class="nx">vpc_id</span>            <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">vpc</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">availability_zone</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_availability_zones</span><span class="p">.</span><span class="nx">available</span><span class="p">.</span><span class="nx">names</span><span class="p">[</span><span class="nx">count</span><span class="p">.</span><span class="nx">index</span><span class="p">]</span>
  <span class="nx">cidr_block</span>        <span class="p">=</span> <span class="nx">cidrsubnet</span><span class="p">(</span><span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">vpc</span><span class="p">.</span><span class="nx">cidr_block</span><span class="p">,</span> <span class="nx">length</span><span class="p">(</span><span class="k">data</span><span class="p">.</span><span class="nx">aws_availability_zones</span><span class="p">.</span><span class="nx">available</span><span class="p">.</span><span class="nx">names</span><span class="p">),</span> <span class="nx">count</span><span class="p">.</span><span class="nx">index</span><span class="p">)</span>

  <span class="nx">tags</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">Name</span> <span class="p">=</span> <span class="s2">"Shared Pool - </span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_availability_zones</span><span class="p">.</span><span class="nx">available</span><span class="p">.</span><span class="nx">names</span><span class="p">[</span><span class="nx">count</span><span class="p">.</span><span class="nx">index</span><span class="p">]</span><span class="k">}</span><span class="s2">"</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_internet_gateway"</span> <span class="s2">"gateway"</span> <span class="p">{</span>
  <span class="nx">vpc_id</span> <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">vpc</span><span class="p">.</span><span class="nx">id</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_default_route_table"</span> <span class="s2">"table"</span> <span class="p">{</span>
  <span class="nx">default_route_table_id</span> <span class="p">=</span> <span class="nx">aws_vpc</span><span class="p">.</span><span class="nx">vpc</span><span class="p">.</span><span class="nx">default_route_table_id</span>

  <span class="nx">route</span> <span class="p">{</span>
    <span class="nx">cidr_block</span> <span class="p">=</span> <span class="s2">"0.0.0.0/0"</span>
    <span class="nx">gateway_id</span> <span class="p">=</span> <span class="nx">aws_internet_gateway</span><span class="p">.</span><span class="nx">gateway</span><span class="p">.</span><span class="nx">id</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Spin this up and you should be able to see the allocation in the IPAM console.</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/12-ipam-allocation.png" alt="IPAM Allocation" /></p>

<p>Switch over to the VPC console in <code class="language-plaintext highlighter-rouge">us-west-2</code> and you should be able to see our subnets.</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/13-vpc-subnets.png" alt="VPC Subnets" /></p>

<h1 id="closing-thoughts">Closing thoughts</h1>

<p>In the end I quite like AWS IPAM. Not sure I’m ever going to have a need for it though. I personally haven’t come across
an AWS network large enough. And while it you could also use it to manage your on-premise network too, that seems like
something that’d be easier to include from the start. But who knows, someone may decide it’s worth importing their
existing environment into it.</p>

<p>Not sure how I feel about the requirements for a pool to be dedicated to a single region. However it does provide that
Region-&gt;Environment link I was looking for.</p>

<p>Actually thinking about it, my current project may have been able to make use of it. But I feel it’s probably not going
to be worth retroactively implementing. Oh well</p>

<p><img src="/assets/posts/playing-around-with-aws-ipam/14-next-time-baby.gif" alt="Next time baby" /></p>

<p>If you have any difficulty following along with any of this I’ve stashed everything on
<a href="https://github.com/incpac/aws-ipam-lab">GitHub.</a></p>]]></content><author><name>Thomas Claridge</name></author><summary type="html"><![CDATA[Story time. Earlier in the month Last month at this point, I was laying out a VPC with some subnets in Terraform. I didn’t want to have to manually enter subnets so wanted to create them dynamically from the CIDR assigned to the VPC. I had forgotten what the cidrsubnet Terraform function was called and went Googling for it.]]></summary></entry><entry><title type="html">SSH Proxy with AWS Systems Manager</title><link href="https://incpac.github.io/posts/2024-03-22-ssh-proxy-with-ssm/" rel="alternate" type="text/html" title="SSH Proxy with AWS Systems Manager" /><published>2024-03-22T00:00:00+13:00</published><updated>2024-03-22T00:00:00+13:00</updated><id>https://incpac.github.io/posts/ssh-proxy-with-ssm</id><content type="html" xml:base="https://incpac.github.io/posts/2024-03-22-ssh-proxy-with-ssm/"><![CDATA[<p>This one is more of a personal note than anything else. Recently I’ve had a need to connect to devices attached to a
host I don’t have any sort of network access to. Thankfully these devices have AWS <del>Simple</del> Systems Manager agents
installed. I can connect to that and set up a SSH proxy to allow me to access the remote devices.</p>

<h1 id="ssh-via-systems-manager">SSH via Systems Manager</h1>

<p>First step is to set up port forwarding via the Systems Manager agent. This will connect local port 2222 to port 22 on
the remote device.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">NODE_ID</span><span class="o">=</span><span class="s2">""</span>
aws ssm start-session <span class="nt">--target</span> <span class="nv">$NODE_ID</span> <span class="nt">--document-name</span> AWS-StartPortForwardingSession <span class="nt">--parameters</span> <span class="s1">'{"portNumber":["22"],"localPortNumber":["2222"]}'</span>
</code></pre></div></div>

<p>The tunnel will operate as long as this command remains running.</p>

<p>From here, you can use SSH to do anything you could if you had a direct connection. For example, SCP files to or from the
device.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SSH_KEY_PATH</span><span class="o">=</span><span class="s2">""</span>
<span class="nb">export </span><span class="nv">SOURCE_PATH</span><span class="o">=</span><span class="s2">""</span>
<span class="nb">export </span><span class="nv">TARGET_PATH</span><span class="o">=</span><span class="s2">""</span>
scp <span class="nt">-P</span> 2222 <span class="nt">-I</span> <span class="nv">$SSH_KEY_PATH</span> <span class="nt">-o</span> <span class="nv">UserKnownHostsFile</span><span class="o">=</span>/dev/null <span class="nt">-o</span> <span class="nv">StrictHostKeyChecking</span><span class="o">=</span>no <span class="nv">$SOURCE_PATH</span> username@localhost:<span class="nv">$TARGET_PATH</span>
</code></pre></div></div>

<h2 id="note">Note</h2>

<p>If you’ve already got a record for <code class="language-plaintext highlighter-rouge">localhost:2222</code> in your known hosts file this would normally fail.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
</code></pre></div></div>

<p>We get around this by setting <code class="language-plaintext highlighter-rouge">UserKnownHostsFile</code> and <code class="language-plaintext highlighter-rouge">StrictHostKeyChecking</code>. You normally wouldn’t want to do this,
however in our situation we re-use the same hostname/port combo for different hosts. The alternative is to either remove
the entries in the known hosts file, or to dedicate one port per SSM host so you don’t run into collisions.</p>

<h1 id="http-proxy">HTTP Proxy</h1>

<p>This is really useful to connect to remote devices over HTTP. With the tunnel still open run the following.</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">SSH_KEY_PATH</span><span class="o">=</span><span class="s2">""</span>
<span class="nb">sudo </span>ssh <span class="nt">-p</span> 2222 <span class="nt">-i</span> <span class="nv">$SSH_KEY_PATH</span> <span class="nt">-ND</span> 9999 <span class="nt">-o</span> <span class="nv">UserKnownHostsFile</span><span class="o">=</span>/dev/null <span class="nt">-o</span> <span class="nv">StrictHostKeyChecking</span><span class="o">=</span>no username@localhost
</code></pre></div></div>

<p>Set your browser proxy config</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Host: 127.0.0.1
Port: 9999
Protocol: SOCKS v5
</code></pre></div></div>

<p>Now just browse to any page on the remote network and your web browser will proxy the request through the SSM agent.</p>]]></content><author><name>Thomas Claridge</name></author><summary type="html"><![CDATA[This one is more of a personal note than anything else. Recently I’ve had a need to connect to devices attached to a host I don’t have any sort of network access to. Thankfully these devices have AWS Simple Systems Manager agents installed. I can connect to that and set up a SSH proxy to allow me to access the remote devices.]]></summary></entry><entry><title type="html">Processing Frames with Lambda and Rust</title><link href="https://incpac.github.io/posts/2024-02-23-processing-frames-with-lambda-and-rust/" rel="alternate" type="text/html" title="Processing Frames with Lambda and Rust" /><published>2024-02-23T00:00:00+13:00</published><updated>2024-02-23T00:00:00+13:00</updated><id>https://incpac.github.io/posts/processing-frames-with-lambda-and-rust</id><content type="html" xml:base="https://incpac.github.io/posts/2024-02-23-processing-frames-with-lambda-and-rust/"><![CDATA[<p><em>This is a continuation of the <a href="/posts/2023-04-16-processing-video-with-mediaconvert-and-lambda/">Processing Video with MediaConvert and Lambda</a> post.</em></p>

<p>With the frames getting stored in S3 the next step is to process them. In this demo we’re going to run a
<a href="https://en.wikipedia.org/wiki/Sobel_operator">Sobel Operator</a> over them before saving in another S3 bucket.</p>

<p>Now my default GoTo for Lambda functions is Python. It’s a popular enough language you can easily find help for.
However, any image processing library I know of includes C libraries that need compiling on install. This causes
problems because (as far as I’m aware) Pip doesn’t support cross-compiling during library install. Which makes packaging
up Lambda functions from my Macbook more pain than I want to deal with for this solution.</p>

<p>So I start thinking “you’ve been meaning to try out Rust for a while, why not now?” Rust has a great
<a href="https://crates.io/crates/lambda_runtime">Lambda tool</a>. You can use it on your local machine and have it build Lambda
compatible binaries.</p>

<p>After getting a simple Hello World working in Lambda, I went on the hunt for a Sobel Operator library. I came across
<a href="https://github.com/dangreco/edgy">Edgy</a> by a guy named Dan Greco. Edgy is a command line app rather than a library,
but I could pull what I needed from it.</p>

<p>From there it was a simple case of implementing the infrastructure to trigger the Lambda on new items to the frame
bucket, having the function process them, then save them into the output bucket.</p>

<h2 id="output-bucket">Output Bucket</h2>

<p>Before we do anything else we need somewhere to store processed frames.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"aws_s3_bucket"</span> <span class="s2">"processed_frames"</span> <span class="p">{</span>
  <span class="nx">bucket</span> <span class="p">=</span> <span class="s2">"mediaconvert-test-processed-frames-${random_string.suffix.result}"</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="modify-lambda-module">Modify Lambda Module</h2>

<p>We’re going to run our frame processor on an arm64 Lambda. Add the following variable to the module.</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">variable</span> <span class="s2">"architectures"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"CPU architectures to run the Lambda function on"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="p">[</span><span class="s2">"x86_64"</span><span class="p">]</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="nx">list</span><span class="err">(</span><span class="nx">string</span><span class="err">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And update the <code class="language-plaintext highlighter-rouge">aws_lambda_function</code> resource</p>

<div class="language-hcl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">resource</span> <span class="s2">"aws_lambda_function"</span> <span class="s2">"lambda_function"</span> <span class="p">{</span>
  <span class="err">...</span>
  <span class="nx">architectures</span> <span class="p">=</span> <span class="nx">var</span><span class="err">.</span><span class="nx">architectures</span>
  <span class="err">...</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="lambda-code">Lambda Code</h2>

<p>In the Lambda functions directory run this to create the function template</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo lambda new frame-processor
</code></pre></div></div>

<p>It’ll ask you some questions:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Is this function an HTTP function? N
AWS Event type that this function receives: s3::S3Event (scroll down to it)
</code></pre></div></div>

<p>Add the following dependencies to the <code class="language-plaintext highlighter-rouge">Cargo.toml</code></p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="py">anyhow</span> <span class="p">=</span> <span class="s">"1.0"</span>
<span class="py">aws-config</span> <span class="p">=</span> <span class="s">"1.1.4"</span>
<span class="py">aws-sdk-s3</span> <span class="p">=</span>  <span class="s">"1.14.0"</span>
<span class="py">image</span> <span class="p">=</span> <span class="s">"0.21.0"</span>
<span class="py">serde</span> <span class="p">=</span> <span class="s">"1.0.196"</span>
<span class="py">serde_json</span> <span class="p">=</span> <span class="s">"1.0.113"</span>
</code></pre></div></div>

<p>In <code class="language-plaintext highlighter-rouge">main.rs</code> we want to import a few libraries. Most of this is standard Lambda stuff. <code class="language-plaintext highlighter-rouge">image</code> is the image library
we’re going to use (duh). <code class="language-plaintext highlighter-rouge">tracing</code> is for logging. <code class="language-plaintext highlighter-rouge">mod sobel</code> refers to the <code class="language-plaintext highlighter-rouge">sobel.rs</code> we’re going to create further
on.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">extern</span> <span class="k">crate</span> <span class="n">aws_config</span><span class="p">;</span>
<span class="k">extern</span> <span class="k">crate</span> <span class="n">aws_lambda_events</span><span class="p">;</span>
<span class="k">extern</span> <span class="k">crate</span> <span class="n">aws_sdk_s3</span><span class="p">;</span>
<span class="k">extern</span> <span class="k">crate</span> <span class="n">image</span><span class="p">;</span>

<span class="k">use</span> <span class="nn">aws_config</span><span class="p">::{</span><span class="n">BehaviorVersion</span><span class="p">,</span> <span class="n">load_defaults</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">aws_lambda_events</span><span class="p">::</span><span class="nn">event</span><span class="p">::</span><span class="nn">s3</span><span class="p">::</span><span class="n">S3Event</span><span class="p">;</span>
<span class="k">use</span> <span class="nn">aws_sdk_s3</span><span class="p">::{</span><span class="n">Client</span><span class="p">,</span> <span class="nn">primitives</span><span class="p">::</span><span class="n">ByteStream</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">lambda_runtime</span><span class="p">::{</span><span class="n">run</span><span class="p">,</span> <span class="n">service_fn</span><span class="p">,</span> <span class="n">Error</span><span class="p">,</span> <span class="n">LambdaEvent</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">std</span><span class="p">::{</span><span class="nn">fs</span><span class="p">::</span><span class="n">File</span><span class="p">,</span> <span class="nn">io</span><span class="p">::</span><span class="n">Write</span><span class="p">,</span> <span class="nn">path</span><span class="p">::</span><span class="n">Path</span><span class="p">,</span> <span class="nn">env</span><span class="p">::</span><span class="n">var</span><span class="p">,</span> <span class="nn">str</span><span class="p">::</span><span class="n">FromStr</span><span class="p">};</span>
<span class="k">use</span> <span class="nn">tracing</span><span class="p">::</span><span class="n">Level</span><span class="p">;</span>

<span class="k">mod</span> <span class="n">sobel</span><span class="p">;</span>
</code></pre></div></div>

<p>A couple of static variables</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="n">TMP_SOURCE_FILE</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"/tmp/source.jpg"</span><span class="p">;</span>
<span class="k">static</span> <span class="n">TMP_OUTPUT_FILE</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span> <span class="o">=</span> <span class="s">"/tmp/output.jpg"</span><span class="p">;</span>
</code></pre></div></div>

<p>A function to process files. This will download the file from S3, run the Sobel Operator on it, then upload it to the
output bucket using the same key as the ingress object.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">fn</span> <span class="nf">handle_record</span><span class="p">(</span><span class="n">client</span><span class="p">:</span> <span class="n">Client</span><span class="p">,</span> <span class="n">bucket</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">,</span> <span class="n">key</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">Error</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="nn">tracing</span><span class="p">::</span><span class="nd">debug!</span><span class="p">({</span><span class="o">%</span><span class="n">bucket</span><span class="p">,</span> <span class="o">%</span><span class="n">key</span><span class="p">},</span> <span class="s">"Handling record"</span><span class="p">);</span>

    <span class="k">if</span> <span class="o">!</span><span class="n">key</span><span class="nf">.ends_with</span><span class="p">(</span><span class="s">".jpg"</span><span class="p">)</span> <span class="p">{</span>
        <span class="nn">tracing</span><span class="p">::</span><span class="nd">info!</span><span class="p">({</span><span class="o">%</span><span class="n">key</span><span class="p">},</span> <span class="s">"File is not a JPEG image, exiting"</span><span class="p">);</span>
        <span class="k">return</span> <span class="nf">Ok</span><span class="p">(())</span>
    <span class="p">}</span>

    <span class="k">let</span> <span class="n">output_bucket</span> <span class="o">=</span> <span class="nf">var</span><span class="p">(</span><span class="s">"OUTPUT_BUCKET"</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">blur_modifier</span> <span class="o">=</span> <span class="nf">var</span><span class="p">(</span><span class="s">"BLUR_MODIFIER"</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">()</span><span class="py">.parse</span><span class="p">::</span><span class="o">&lt;</span><span class="nb">i32</span><span class="o">&gt;</span><span class="p">()</span><span class="nf">.unwrap</span><span class="p">();</span>

    <span class="k">let</span> <span class="k">mut</span> <span class="n">source_file</span> <span class="o">=</span> <span class="nn">File</span><span class="p">::</span><span class="nf">create</span><span class="p">(</span><span class="o">&amp;</span><span class="n">TMP_SOURCE_FILE</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">object</span> <span class="o">=</span> <span class="n">client</span>
        <span class="nf">.get_object</span><span class="p">()</span>
        <span class="nf">.bucket</span><span class="p">(</span><span class="n">bucket</span><span class="p">)</span>
        <span class="nf">.key</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
        <span class="nf">.send</span><span class="p">()</span>
        <span class="k">.await</span><span class="o">?</span><span class="p">;</span>

    <span class="k">while</span> <span class="k">let</span> <span class="nf">Some</span><span class="p">(</span><span class="n">bytes</span><span class="p">)</span> <span class="o">=</span> <span class="n">object</span><span class="py">.body</span><span class="nf">.try_next</span><span class="p">()</span><span class="k">.await</span><span class="o">?</span> <span class="p">{</span>
        <span class="n">source_file</span><span class="nf">.write_all</span><span class="p">(</span><span class="o">&amp;</span><span class="n">bytes</span><span class="p">)</span><span class="o">?</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="nn">tracing</span><span class="p">::</span><span class="nd">debug!</span><span class="p">({</span><span class="o">%</span><span class="n">key</span><span class="p">,</span> <span class="o">%</span><span class="n">TMP_SOURCE_FILE</span><span class="p">},</span> <span class="s">"Downloaded file from S3 to local disk"</span><span class="p">);</span>

    <span class="nn">sobel</span><span class="p">::</span><span class="nf">process_image</span><span class="p">(</span><span class="n">TMP_SOURCE_FILE</span><span class="p">,</span> <span class="n">TMP_OUTPUT_FILE</span><span class="p">,</span> <span class="n">blur_modifier</span><span class="p">);</span>
    <span class="nn">tracing</span><span class="p">::</span><span class="nd">debug!</span><span class="p">({</span><span class="o">%</span><span class="n">TMP_SOURCE_FILE</span><span class="p">,</span> <span class="o">%</span><span class="n">TMP_OUTPUT_FILE</span><span class="p">,</span> <span class="o">%</span><span class="n">blur_modifier</span><span class="p">},</span> <span class="s">"Finished processing image"</span><span class="p">);</span>

    <span class="k">let</span> <span class="n">body</span> <span class="o">=</span> <span class="nn">ByteStream</span><span class="p">::</span><span class="nf">from_path</span><span class="p">(</span><span class="nn">Path</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">TMP_OUTPUT_FILE</span><span class="p">))</span><span class="k">.await</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">_</span> <span class="o">=</span> <span class="n">client</span>
        <span class="nf">.put_object</span><span class="p">()</span>
        <span class="nf">.bucket</span><span class="p">(</span><span class="o">&amp;</span><span class="n">output_bucket</span><span class="p">)</span>
        <span class="nf">.key</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
        <span class="nf">.body</span><span class="p">(</span><span class="n">body</span><span class="nf">.unwrap</span><span class="p">())</span>
        <span class="nf">.send</span><span class="p">()</span>
        <span class="k">.await</span><span class="p">;</span>
    <span class="nn">tracing</span><span class="p">::</span><span class="nd">debug!</span><span class="p">({</span><span class="o">%</span><span class="n">TMP_OUTPUT_FILE</span><span class="p">,</span> <span class="o">%</span><span class="n">output_bucket</span><span class="p">,</span> <span class="o">%</span><span class="n">key</span><span class="p">},</span> <span class="s">"Uploaded output file to S3"</span><span class="p">);</span>

    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We need to update the <code class="language-plaintext highlighter-rouge">function_handler()</code> to process the incoming <code class="language-plaintext highlighter-rouge">S3Event</code>.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[tracing::instrument(skip(event),</span> <span class="nd">fields(req_id</span> <span class="nd">=</span> <span class="err">%</span><span class="nd">event</span><span class="err">.</span><span class="nd">context</span><span class="err">.</span><span class="nd">request_id))]</span>
<span class="k">async</span> <span class="k">fn</span> <span class="nf">function_handler</span><span class="p">(</span><span class="n">event</span><span class="p">:</span> <span class="n">LambdaEvent</span><span class="o">&lt;</span><span class="n">S3Event</span><span class="o">&gt;</span><span class="p">)</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">Error</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="k">let</span> <span class="n">aws_config</span> <span class="o">=</span> <span class="nf">load_defaults</span><span class="p">(</span><span class="nn">BehaviorVersion</span><span class="p">::</span><span class="nf">v2023_11_09</span><span class="p">())</span><span class="k">.await</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">client</span> <span class="o">=</span> <span class="nn">Client</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="o">&amp;</span><span class="n">aws_config</span><span class="p">);</span>

    <span class="k">for</span> <span class="n">record</span> <span class="k">in</span> <span class="n">event</span><span class="py">.payload.records</span><span class="nf">.iter</span><span class="p">()</span> <span class="p">{</span>
        <span class="k">let</span> <span class="n">bucket</span> <span class="o">=</span> <span class="n">record</span><span class="py">.s3.bucket.name</span><span class="nf">.clone</span><span class="p">()</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"Could not get bucket name from record"</span><span class="p">);</span>
        <span class="k">let</span> <span class="n">key</span> <span class="o">=</span> <span class="n">record</span><span class="py">.s3.object.key</span><span class="nf">.clone</span><span class="p">()</span><span class="nf">.expect</span><span class="p">(</span><span class="s">"Could not get key from object record"</span><span class="p">);</span>

        <span class="nn">tracing</span><span class="p">::</span><span class="nd">debug!</span><span class="p">({</span><span class="o">%</span><span class="n">bucket</span><span class="p">,</span> <span class="o">%</span><span class="n">key</span><span class="p">},</span> <span class="s">"Received new file"</span><span class="p">);</span>

        <span class="k">match</span> <span class="nf">handle_record</span><span class="p">(</span><span class="n">client</span><span class="nf">.clone</span><span class="p">(),</span> <span class="o">&amp;</span><span class="n">bucket</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">key</span><span class="p">)</span><span class="k">.await</span> <span class="p">{</span>
            <span class="nf">Ok</span><span class="p">(())</span> <span class="k">=&gt;</span> <span class="p">{</span>
                <span class="nn">tracing</span><span class="p">::</span><span class="nd">info!</span><span class="p">({</span><span class="o">%</span><span class="n">bucket</span><span class="p">,</span> <span class="o">%</span><span class="n">key</span><span class="p">},</span> <span class="s">"Processed file"</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="nf">Err</span><span class="p">(</span><span class="n">err</span><span class="p">)</span> <span class="k">=&gt;</span> <span class="p">{</span>
                <span class="nn">tracing</span><span class="p">::</span><span class="nd">error!</span><span class="p">({</span><span class="o">%</span><span class="n">bucket</span><span class="p">,</span> <span class="o">%</span><span class="n">key</span><span class="p">,</span> <span class="o">%</span><span class="n">err</span><span class="p">},</span> <span class="s">"Failed to process file"</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="nf">Ok</span><span class="p">(())</span>
<span class="p">}</span>
</code></pre></div></div>

<p>These events contain one or more records. Each record is a file that has been uploaded to S3. We pull the object key and
bucket name from the record and make a call to the <code class="language-plaintext highlighter-rouge">handle_record()</code> function.</p>

<p>In the <code class="language-plaintext highlighter-rouge">main()</code> function, we’re going to update the logging functionality.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">#[tokio::main]</span>
<span class="k">async</span> <span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="k">-&gt;</span> <span class="nb">Result</span><span class="o">&lt;</span><span class="p">(),</span> <span class="n">Error</span><span class="o">&gt;</span> <span class="p">{</span>
    <span class="nn">tracing_subscriber</span><span class="p">::</span><span class="nf">fmt</span><span class="p">()</span><span class="nf">.json</span><span class="p">()</span>
        <span class="nf">.with_max_level</span><span class="p">(</span><span class="nn">Level</span><span class="p">::</span><span class="nf">from_str</span><span class="p">(</span><span class="o">&amp;</span><span class="nn">std</span><span class="p">::</span><span class="nn">env</span><span class="p">::</span><span class="nf">var</span><span class="p">(</span><span class="s">"LOG_LEVEL"</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">())</span><span class="nf">.unwrap</span><span class="p">())</span>
        <span class="nf">.with_current_span</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>   <span class="c1">// remove duplicate information from the logs</span>
        <span class="nf">.without_time</span><span class="p">()</span>             <span class="c1">// disabling time is handy because CloudWatch will add the ingestion time.</span>
        <span class="nf">.with_target</span><span class="p">(</span><span class="k">false</span><span class="p">)</span>         <span class="c1">// disable printing the name of the module in every log line.</span>
        <span class="nf">.init</span><span class="p">();</span>
    <span class="nf">run</span><span class="p">(</span><span class="nf">service_fn</span><span class="p">(</span><span class="n">function_handler</span><span class="p">))</span><span class="k">.await</span>
<span class="p">}</span>
</code></pre></div></div>

<p>What we’ve got here will take the event details from the S3 trigger, iterate over ever record in the event, and process
each image using the <code class="language-plaintext highlighter-rouge">process_image()</code> function from the <code class="language-plaintext highlighter-rouge">sobel</code> module.</p>

<p>For the module we need to create <code class="language-plaintext highlighter-rouge">sobel.rs</code> in the <code class="language-plaintext highlighter-rouge">src</code> directory. In here we’re going to refactor some of the <code class="language-plaintext highlighter-rouge">Edgy</code>
app.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">use</span> <span class="nn">image</span><span class="p">::{</span><span class="n">GenericImageView</span><span class="p">,</span> <span class="n">ImageBuffer</span><span class="p">,</span> <span class="n">Luma</span><span class="p">};</span>

<span class="k">pub</span> <span class="k">fn</span> <span class="nf">process_image</span><span class="p">(</span><span class="n">source_filename</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">,</span> <span class="n">output_filename</span><span class="p">:</span> <span class="o">&amp;</span><span class="nb">str</span><span class="p">,</span> <span class="n">blur_modifier</span><span class="p">:</span> <span class="nb">i32</span><span class="p">)</span> <span class="p">{</span>
    <span class="nn">tracing</span><span class="p">::</span><span class="nd">debug!</span><span class="p">({</span><span class="o">%</span><span class="n">source_filename</span><span class="p">,</span> <span class="o">%</span><span class="n">output_filename</span><span class="p">,</span> <span class="o">%</span><span class="n">blur_modifier</span><span class="p">},</span> <span class="s">"Starting image processing"</span><span class="p">);</span>
    <span class="k">let</span> <span class="n">source</span> <span class="o">=</span> <span class="nn">image</span><span class="p">::</span><span class="nf">open</span><span class="p">(</span><span class="n">source_filename</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span>
    <span class="k">let</span> <span class="p">(</span><span class="n">width</span><span class="p">,</span> <span class="n">height</span><span class="p">)</span> <span class="o">=</span> <span class="n">source</span><span class="nf">.dimensions</span><span class="p">();</span>
    <span class="k">let</span> <span class="n">sigma</span> <span class="o">=</span> <span class="p">(((</span><span class="n">width</span> <span class="o">*</span> <span class="n">height</span><span class="p">)</span> <span class="k">as</span> <span class="nb">f32</span><span class="p">)</span> <span class="o">/</span> <span class="mf">3630000.0</span><span class="p">)</span> <span class="o">*</span> <span class="n">blur_modifier</span> <span class="k">as</span> <span class="nb">f32</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">gaussed</span> <span class="o">=</span> <span class="n">source</span><span class="nf">.blur</span><span class="p">(</span><span class="n">sigma</span><span class="p">);</span>
    <span class="k">let</span> <span class="n">gray</span> <span class="o">=</span> <span class="n">gaussed</span><span class="nf">.to_luma</span><span class="p">();</span>

    <span class="k">let</span> <span class="n">sobel_width</span><span class="p">:</span><span class="nb">u32</span> <span class="o">=</span> <span class="n">gray</span><span class="nf">.width</span><span class="p">()</span><span class="o">-</span><span class="mi">2</span><span class="p">;</span>
    <span class="k">let</span> <span class="n">sobel_height</span><span class="p">:</span><span class="nb">u32</span> <span class="o">=</span> <span class="n">gray</span><span class="nf">.height</span><span class="p">()</span><span class="o">-</span><span class="mi">2</span><span class="p">;</span>
    <span class="k">let</span> <span class="k">mut</span> <span class="n">buff</span><span class="p">:</span><span class="n">ImageBuffer</span><span class="o">&lt;</span><span class="n">Luma</span><span class="o">&lt;</span><span class="nb">u8</span><span class="o">&gt;</span><span class="p">,</span> <span class="nb">Vec</span><span class="o">&lt;</span><span class="nb">u8</span><span class="o">&gt;&gt;</span> <span class="o">=</span> <span class="nn">ImageBuffer</span><span class="p">::</span><span class="nf">new</span><span class="p">(</span><span class="n">sobel_width</span><span class="p">,</span> <span class="n">sobel_height</span><span class="p">);</span>

    <span class="k">for</span> <span class="n">i</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="n">sobel_width</span> <span class="p">{</span>
        <span class="k">for</span> <span class="n">j</span> <span class="k">in</span> <span class="mi">0</span><span class="o">..</span><span class="n">sobel_height</span> <span class="p">{</span>
            <span class="k">let</span> <span class="n">val0</span> <span class="o">=</span> <span class="n">gray</span><span class="nf">.get_pixel</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">)</span><span class="py">.data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">val1</span> <span class="o">=</span> <span class="n">gray</span><span class="nf">.get_pixel</span><span class="p">(</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="n">j</span><span class="p">)</span><span class="py">.data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">val2</span> <span class="o">=</span> <span class="n">gray</span><span class="nf">.get_pixel</span><span class="p">(</span><span class="n">i</span><span class="o">+</span><span class="mi">2</span><span class="p">,</span> <span class="n">j</span><span class="p">)</span><span class="py">.data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">val3</span> <span class="o">=</span> <span class="n">gray</span><span class="nf">.get_pixel</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="o">+</span><span class="mi">1</span><span class="p">)</span><span class="py">.data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">val5</span> <span class="o">=</span> <span class="n">gray</span><span class="nf">.get_pixel</span><span class="p">(</span><span class="n">i</span><span class="o">+</span><span class="mi">2</span><span class="p">,</span> <span class="n">j</span><span class="o">+</span><span class="mi">1</span><span class="p">)</span><span class="py">.data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">val6</span> <span class="o">=</span> <span class="n">gray</span><span class="nf">.get_pixel</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="o">+</span><span class="mi">2</span><span class="p">)</span><span class="py">.data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">val7</span> <span class="o">=</span> <span class="n">gray</span><span class="nf">.get_pixel</span><span class="p">(</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="p">,</span> <span class="n">j</span><span class="o">+</span><span class="mi">2</span><span class="p">)</span><span class="py">.data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">val8</span> <span class="o">=</span> <span class="n">gray</span><span class="nf">.get_pixel</span><span class="p">(</span><span class="n">i</span><span class="o">+</span><span class="mi">2</span><span class="p">,</span> <span class="n">j</span><span class="o">+</span><span class="mi">2</span><span class="p">)</span><span class="py">.data</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="k">as</span> <span class="nb">i32</span><span class="p">;</span>

            <span class="k">let</span> <span class="n">gx</span> <span class="o">=</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="o">*</span><span class="n">val0</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="o">*</span><span class="n">val3</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="o">*</span><span class="n">val6</span><span class="p">)</span> <span class="o">+</span> <span class="n">val2</span> <span class="o">+</span> <span class="p">(</span><span class="mi">2</span><span class="o">*</span><span class="n">val5</span><span class="p">)</span> <span class="o">+</span> <span class="n">val8</span><span class="p">;</span>
            <span class="k">let</span> <span class="n">gy</span> <span class="o">=</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="o">*</span><span class="n">val0</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="o">*</span><span class="n">val1</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="o">*</span><span class="n">val2</span><span class="p">)</span> <span class="o">+</span> <span class="n">val6</span> <span class="o">+</span> <span class="p">(</span><span class="mi">2</span><span class="o">*</span><span class="n">val7</span><span class="p">)</span> <span class="o">+</span> <span class="n">val8</span><span class="p">;</span>

            <span class="k">let</span> <span class="k">mut</span> <span class="n">mag</span> <span class="o">=</span> <span class="p">((</span><span class="n">gx</span> <span class="k">as</span> <span class="nb">f64</span><span class="p">)</span><span class="nf">.powi</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="o">+</span> <span class="p">(</span><span class="n">gy</span> <span class="k">as</span> <span class="nb">f64</span><span class="p">)</span><span class="nf">.powi</span><span class="p">(</span><span class="mi">2</span><span class="p">))</span><span class="nf">.sqrt</span><span class="p">();</span>

            <span class="k">if</span> <span class="n">mag</span> <span class="o">&gt;</span> <span class="mf">255.0</span> <span class="p">{</span>
                <span class="n">mag</span> <span class="o">=</span> <span class="mf">255.0</span><span class="p">;</span>
            <span class="p">}</span>

            <span class="n">buff</span><span class="nf">.put_pixel</span><span class="p">(</span><span class="n">i</span><span class="p">,</span> <span class="n">j</span><span class="p">,</span> <span class="nf">Luma</span><span class="p">([</span><span class="n">mag</span> <span class="k">as</span> <span class="nb">u8</span><span class="p">]));</span>
        <span class="p">}</span>
    <span class="p">}</span>


    <span class="n">buff</span><span class="nf">.save</span><span class="p">(</span><span class="n">output_filename</span><span class="p">)</span><span class="nf">.unwrap</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>You can compile the function with</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cargo lambda build --release --arm64 --output-format binary
</code></pre></div></div>

<p>This will create a <code class="language-plaintext highlighter-rouge">target/lambda/frame-processor</code> directory that contains everything we need to upload to AWS.</p>

<h2 id="lambda-function-infrastructure">Lambda Function Infrastructure</h2>

<p>Now that we’ve built our executable, we need to create the function in AWS. First we define the access our function
gets.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_iam_policy_document"</span> <span class="s2">"frame_processor"</span> <span class="p">{</span>
  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">sid</span> <span class="p">=</span> <span class="s2">"WriteToCloudWatchLogs"</span>

    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
      <span class="s2">"logs:CreateLogGroup"</span><span class="p">,</span>
      <span class="s2">"logs:CreateLogStream"</span><span class="p">,</span>
      <span class="s2">"logs:PutLogEvents"</span><span class="p">,</span>
    <span class="p">]</span>

    <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"*"</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">sid</span> <span class="p">=</span> <span class="s2">"ReadFilesFromRawFramesBucket"</span>

    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
      <span class="s2">"s3:GetObject"</span>
    <span class="p">]</span>

    <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span>
      <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">raw_frames</span><span class="p">.</span><span class="nx">arn</span><span class="p">,</span>
      <span class="s2">"</span><span class="k">${</span><span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">raw_frames</span><span class="p">.</span><span class="nx">arn</span><span class="k">}</span><span class="s2">/*"</span><span class="p">,</span>
    <span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">sid</span> <span class="p">=</span> <span class="s2">"WriteFilesToProcessedFramesBucket"</span>

    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
      <span class="s2">"s3:PutObject"</span>
    <span class="p">]</span>

    <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span>
      <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">processed_frames</span><span class="p">.</span><span class="nx">arn</span><span class="p">,</span>
      <span class="s2">"</span><span class="k">${</span><span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">processed_frames</span><span class="p">.</span><span class="nx">arn</span><span class="k">}</span><span class="s2">/*"</span>
    <span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This lets it pull images from the MediaConvert output and write to our Processed Frames bucket. It also grants
permissions to write logs.</p>

<p>For the function itself we’re going to make use of our <code class="language-plaintext highlighter-rouge">lambda_function</code> module.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="s2">"frame_processor"</span> <span class="p">{</span>
  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"./modules/lambda_function"</span>

  <span class="nx">function_name</span> <span class="p">=</span> <span class="s2">"mediaconvert-frame-processor-</span><span class="k">${</span><span class="nx">random_string</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">iam_policy</span>    <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_iam_policy_document</span><span class="p">.</span><span class="nx">frame_processor</span><span class="p">.</span><span class="nx">json</span>
  <span class="nx">runtime</span>       <span class="p">=</span> <span class="s2">"provided.al2"</span>
  <span class="nx">architectures</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"arm64"</span><span class="p">]</span>
  <span class="nx">handler</span>       <span class="p">=</span> <span class="s2">"bootstrap"</span>
  <span class="nx">source_dir</span>    <span class="p">=</span> <span class="s2">"</span><span class="k">${</span><span class="nx">path</span><span class="p">.</span><span class="nx">root</span><span class="k">}</span><span class="s2">/lambda_functions/frame_processor/target/lambda/frame-processor"</span>
  <span class="nx">timeout</span>       <span class="p">=</span> <span class="mi">120</span>

  <span class="nx">environment_variables</span> <span class="p">=</span> <span class="p">{</span>
    <span class="s2">"OUTPUT_BUCKET"</span> <span class="p">=</span> <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">processed_frames</span><span class="p">.</span><span class="nx">bucket</span>
    <span class="s2">"BLUR_MODIFIER"</span> <span class="p">=</span> <span class="mi">8</span>
    <span class="s2">"LOG_LEVEL"</span>     <span class="p">=</span> <span class="s2">"INFO"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">BLUR_MODIFIER</code> variable adjusts how much blur is used when processing the image. Adjust this until you find the
sweet spot.</p>

<p>Next we need to connect the function to the S3 bucket. This is done with a <code class="language-plaintext highlighter-rouge">aws_s3_bucket_notification</code> resource. We
want to filter for <code class="language-plaintext highlighter-rouge">ObjectCreated</code> events so the function gets run every time a new image gets uploaded to the bucket.</p>

<p>To allow the bucket to trigger the Lambda function we need an <code class="language-plaintext highlighter-rouge">aws_lambda_function</code> resource. This grants the bucket the
<code class="language-plaintext highlighter-rouge">InvokeFunction</code> permission.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_lambda_permission"</span> <span class="s2">"frame_processor_trigger"</span> <span class="p">{</span>
  <span class="nx">statement_id</span>  <span class="p">=</span> <span class="s2">"AllowExecutionFromS3Bucket"</span>
  <span class="nx">action</span>        <span class="p">=</span> <span class="s2">"lambda:InvokeFunction"</span>
  <span class="nx">function_name</span> <span class="p">=</span> <span class="k">module</span><span class="p">.</span><span class="nx">frame_processor</span><span class="p">.</span><span class="nx">function_arn</span>
  <span class="nx">principal</span>     <span class="p">=</span> <span class="s2">"s3.amazonaws.com"</span>
  <span class="nx">source_arn</span>    <span class="p">=</span> <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">raw_frames</span><span class="p">.</span><span class="nx">arn</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_s3_bucket_notification"</span> <span class="s2">"frame_processor_trigger"</span> <span class="p">{</span>
  <span class="nx">bucket</span> <span class="p">=</span> <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">raw_frames</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">lambda_function</span> <span class="p">{</span>
    <span class="nx">lambda_function_arn</span> <span class="p">=</span> <span class="k">module</span><span class="p">.</span><span class="nx">frame_processor</span><span class="p">.</span><span class="nx">function_arn</span>
    <span class="nx">events</span>              <span class="p">=</span> <span class="p">[</span><span class="s2">"s3:ObjectCreated:*"</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">depends_on</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_lambda_permission</span><span class="p">.</span><span class="nx">frame_processor_trigger</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="testing">Testing</h2>

<p>I’m sure once you’ve got all this deploy you’re going to want to test it. The complete method would be to upload a video
to the ingress bucket. However, if you can’t be bothered waiting for MediaConvert, you can simply upload a JPEG to the
frames bucket. It needs to have a <code class="language-plaintext highlighter-rouge">.jpg</code> file extension as we filter for it. If everything’s working correctly you
should see a file show up in the ProcessedFrames bucket.</p>

<p>If you’re having trouble, check out the
<a href="https://github.com/incpac/mediaconvert-frame-processor/commit/5db3050379bf534abbe8458cd35beb48676d5768">complete diff</a>
for all changes made.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>With this we’ve got a pipeline that rips videos into frames and processes them. Next steps would be to update the
FrameProcessor to track the number of frames we’ve processed. We currently store the total number of frames in DynamoDB
but we may want to reconsider that due to the number of IO requests per video.</p>

<p>Once that’s done we’ll be able to tell when we’ve processed all frames for a particular video and we can stitch it back
together again. I’ll aim to get that sorted sooner than the 10 months this post took.</p>]]></content><author><name>Thomas Claridge</name></author><summary type="html"><![CDATA[This is a continuation of the Processing Video with MediaConvert and Lambda post.]]></summary></entry><entry><title type="html">Running a Private Apt Repository in S3</title><link href="https://incpac.github.io/posts/2023-10-13-private-apt-repo-in-s3/" rel="alternate" type="text/html" title="Running a Private Apt Repository in S3" /><published>2023-10-13T00:00:00+13:00</published><updated>2023-10-13T00:00:00+13:00</updated><id>https://incpac.github.io/posts/private-apt-repo-in-s3</id><content type="html" xml:base="https://incpac.github.io/posts/2023-10-13-private-apt-repo-in-s3/"><![CDATA[<p>Recently I was asked to assist with developing a software distribution system for a bunch of remote, IoT style devices
that connect directly to a VPC and have no real internet access.</p>

<p>There was a mixture of in-house stuff written in Python, Bash, and Golang that would either be written directly 
to files on the device, or binaries copied from from S3 and placed where needed. There were also externally sourced
packages that needed to be installed. Mostly in Deb format. These were also housed in S3. Installation and configuration 
would then be controlled using Ansible.</p>

<p>Different pieces of software were managed by different teams. As you can imagine, this meant keeping track of what’s 
getting installed where, and keeping versioning and dependency management straight started to become difficult.</p>

<p>To help clean all this up and ensure a uniform method of distributing and installing software it was decided that we 
would package everything up into Deb packages and create our own Apt repository.</p>

<p>This provided the benefet of having multiple versions of software available to us. Each package listed its own 
dependencies and Aptitude will handle finding and installing them for us. The team managing the IoT devices didn’t 
need to know what runtime would be required for a certain piece of software or what would be required to get it working;
all of that was the responsibility of the package maintainer.</p>

<p>When it came to actually creating the repo, we wanted to run this out of S3. There are a few tools to help us build a 
repository, but most of them wanted to create the entire respository locally, then sync it up with your target bucket.
Since we were building and releasing packages individually, we wanted to avoid this. The best tool for our usecase we 
found was <a href="https://github.com/deb-s3/deb-s3">deb-s3.</a> This would let us create the repository and manage packages 
on a per-package basis.</p>

<p>Now that we’ve got the bones of a repository, we need to ensure we can access it from the IoT devices. The quickest way 
would be to enable a static website on the S3 bucket. The problem with this is that it opens the repo up to the world.
We wouldn’t be able to control who can access it. Additionally, since our IoT devices didn’t have internet access, they 
wouldn’t be able to access it themselves. What we needed was some way to make Apt talk S3 via a VPC Endpoint.</p>

<p>Thankfully we also didn’t have to create this from scratch either. The 
<a href="https://github.com/MayaraCloud/apt-transport-s3">apt-transport-s3</a> Apt method provided much of the functionality we 
were looking for. We took that, replaced the credentials part with something that was compatible with our IoT devices,
and deployed it via Ansible.</p>

<p>With all that sorted we now had a uniform way we could release software and updates to our IoT devices and anything 
else we desired.</p>

<h1 id="building">Building</h1>

<p>Here I’m going to go over a quick demo of how to set something like this up. I’m not going to set up a network that’s 
an exact copy of ours. This one will have internet access and allow the EC2 to have access to the standard S3 Endpoint.</p>

<p><strong>Set up the repository</strong></p>
<ol>
  <li>Set up the Terraform environment located in <a href="https://github.com/incpac/s3-apt-repo-demo">this repository</a>: 
<code class="language-plaintext highlighter-rouge">terraform apply -var="ssh_key=name-of-my-ssh-key-uploaded-to-ec2"</code></li>
  <li>Use SCP to copy the test HelloWorld package to the EC2 instance</li>
  <li>Connect to the EC2 instance and run the following to build our test package: <code class="language-plaintext highlighter-rouge">bash helloworld/build.sh</code>. Note that you need 
to be in the parent directory of the package.</li>
  <li>You can use <code class="language-plaintext highlighter-rouge">dpkg</code> to install this package to test. Run it with <code class="language-plaintext highlighter-rouge">helloworld</code>. Don’t forget to remove it afterwards</li>
  <li>Install Ruby: <code class="language-plaintext highlighter-rouge">sudo apt install -y ruby</code></li>
  <li>Install <a href="https://github.com/deb-s3/deb-s3">deb-s3</a>: <code class="language-plaintext highlighter-rouge">sudo gem install deb-s3</code></li>
  <li>Publish the package to S3:
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>deb-s3 upload \
  --bucket apt-repo-test-blah-blah-blah \
  --s3-region my-aws-region \
  --visibility private \
  --preserve-versions \
  helloworld_0.1_all.deb 
</code></pre></div>    </div>
  </li>
</ol>

<p><strong>Configure Apt to pull from the repo</strong><br />
<em>This one doesn’t need to be on the same instance as the first task</em></p>

<ol>
  <li>Install <a href="https://github.com/MayaraCloud/apt-transport-s3">apt-transport-s3</a></li>
  <li>Add the following to your <code class="language-plaintext highlighter-rouge">/etc/apt/sources.list</code>
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>deb [trusted=yes] s3://apt-repo-test-blah-blah-blah/ stable main 
</code></pre></div>    </div>
  </li>
  <li>Create <code class="language-plaintext highlighter-rouge">/etc/apt/s3auth.conf</code> with
    <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Region = 'ap-southeast-2'
</code></pre></div>    </div>
  </li>
  <li>Test
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install </span>helloworld
helloworld
</code></pre></div>    </div>
  </li>
</ol>

<h1 id="follow-up">Follow Up</h1>

<p>This builds a basic setup with a single EC2 instance pointing to a bucket. Some things you probably want to do afterwards 
include configuring <code class="language-plaintext highlighter-rouge">apt-transport-s3</code> to use a VPC Endpoint and setting up signing for the Deb packages</p>

<h1 id="known-issues">Known Issues</h1>

<p>Version v2.1.0 of the apt-transport-s3 package didn’t work out the door. I received the following error when running 
<code class="language-plaintext highlighter-rouge">apt update</code></p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Traceback (most recent call last):
  File "/usr/lib/apt/methods/s3", line 639, in &lt;module&gt;
    method = S3_method(config)
  File "/usr/lib/apt/methods/s3", line 426, in __init__
    self.iam.get_credentials()
  File "/usr/lib/apt/methods/s3", line 234, in get_credentials
    str(self.iamrole, 'utf-8')))
TypeError: decoding str is not supported
</code></pre></div></div>

<p>This was resolved by updating line 234 from <code class="language-plaintext highlighter-rouge">str(self.iamrole, 'utf-8')))</code> to <code class="language-plaintext highlighter-rouge">self.iamrole))</code></p>]]></content><author><name>Thomas Claridge</name></author><summary type="html"><![CDATA[Recently I was asked to assist with developing a software distribution system for a bunch of remote, IoT style devices that connect directly to a VPC and have no real internet access.]]></summary></entry><entry><title type="html">Processing Video with MediaConvert and Lambda</title><link href="https://incpac.github.io/posts/2023-04-16-processing-video-with-mediaconvert-and-lambda/" rel="alternate" type="text/html" title="Processing Video with MediaConvert and Lambda" /><published>2023-04-16T00:00:00+12:00</published><updated>2023-04-16T00:00:00+12:00</updated><id>https://incpac.github.io/posts/processing-video-with-mediaconvert-and-lambda</id><content type="html" xml:base="https://incpac.github.io/posts/2023-04-16-processing-video-with-mediaconvert-and-lambda/"><![CDATA[<p><strong>Story Time:</strong> So one day I’m just sitting at my desk and my manager gets all up in my business with “Yo, you with the face. 
Come with me.”</p>

<p>He takes me across the carpark to another building where I get introduced to one of our developers. We have a chat about a 
project he’s been working on. It takes a GoPro video, runs each frame through a machine learning algorithm, and spits out a 
JSON file at the end.</p>

<p>Some pretty awesome shit.</p>

<p>Unfortunately going frame-by-frame each video ends up taking a while. The dev will load up a queue on Friday and let it run 
over the weekend.</p>

<p>Bossman starts turning starfish shaped and says “we should take this project, and push it somewhere else.” Naturally, by 
Somewhere Else he meant AWS. And by We he meant me.</p>

<p>So I did.</p>

<p>The goal here was to reduce the time it takes to process a video, likely by processing frames in parallel. Ideally we’d toss 
them in Lambda triggered off EventBridge when the frame shows up in S3. The alternative would be to just chuck it in EC2, but 
that would run into the same compute limitations we had with the physical hardware.</p>

<p>Now don’t get me wrong, this wasn’t easy. First thing was to get the processing into Lambda. Of course the whole thing was way 
too large. Tensorflow itself wouldn’t fit in Lambda at the time. Even once we got that sorted, the issue relevant to this post 
was how to split the video into frames.</p>

<p>Splitting a video into frames isn’t actually too hard. You could do this in Python with OpenCV, or from the CLI with FFMPEG. 
Our real problem was getting enough ephemeral storage in Lambda to stash not only the source video, but also each of the 65k 
frames that make it up, and getting it all done in the before the timeout.</p>

<p>With the 500MB of disk Lambda gave us at the time we could either stash the video or the frames but not both. Sweet as, what if 
we skip splitting the video and do it straight in the frame processing. OK, so you want to pull the video from S3 65k times 
each video? Hell no. Even leaving the video in memory for warm Lambdas, the S3 costs would blow out the bill.</p>

<p>OK, how about stashing it on an EFS disk? This worked fine for our 2 second test video, but once we tossed a couple of full ones 
at it, the disk started throttling. While we could increase the IO, we kept running into the same issue.</p>

<p>Sweet as, Lambda’s out. ECS is the next obvious choice. Aaanndd… same issues as Lambda. Lack of enough ephemeral storage.</p>

<p>At this point I’d given up and had started working on getting an EC2 AMI created.</p>

<p>In walks an AWS SA, here for an Immersion Day the boss was running. Noticing the roughly head shaped dent in my desk, he asks 
how the project was going.</p>

<p>After unloading in a way that would make a therapist take up day drinking, he asks me “have you looked at MediaConvert?” Now, I 
had actually come across MediaConvert in my research. While it won’t split the video into frames, it will take stills at a set 
rate. All the documentation indicated that you’d be looking to only grab a frame every few seconds, with the intention of using
them for promotional material or something. His response? Do it anyway. Hard to argue with that. The result was bloody epic.</p>

<p>This had one problem. We can only grab frames x times per second. The video that gets uploaded is recorded at 29.38 FPS (or 
something equally not easily divisible by a round number) and I’d previously been informed that the telemetry is at the same 
rate. It’s at this point the dev on the project pipes up “actually, the telemetry is at 30 FPS, it’s just that it’s close 
enough to the video that I’ve been treating it as one-for-one.”</p>

<p>Office supplies may or may not have been thrown at this point. Reports vary.</p>

<p>Hacking away at it for a bit longer, we managed to get the whole process down to about five minutes. Fuck yeah. Even more fine 
tuning, ripping out unnecessary functionality, and decreasing the processed frame rate to 5 FPS (we didn’t actually need to 
process every frame) we were able to get a video through at 1:40ish at the fastest.</p>

<p>Now of course as additional functionality got added, that time’s blown out again. But still nowhere near the original 6 hours 
it used to take.</p>

<p>If you’re interested in reading more checkout the 
<a href="https://mckinnel.me/massively-parallel-machine-learning-inference-using-aws-lambda.html">boss’s write up.</a> There’s also a This
Is My Architecture on <a href="https://www.youtube.com/watch?v=cZuoiXQ0xUk">Youtube.</a> Otherwise continue on for how to get something 
similar up and running.</p>

<h2 id="something-similar">Something Similar</h2>

<p>First thing we’re going to do is create a Terraform module to handle Lambda function creation. This is because we’re going to be 
using a few of them and they get real repetitive. In <code class="language-plaintext highlighter-rouge">modules/lambda_function/main.tf</code></p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">variable</span> <span class="s2">"environment_variables"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Dict of environment variables to attach to the Lambda function"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="kc">null</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"function_name"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Name of the Lambda function and related resources"</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"handler"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Function entrypoint"</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"iam_policy"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"IAM policy to attach to the Lambda Function in JSON"</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"runtime"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Lambda Runtime to deploy"</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"source_dir"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Directory for the Lambda Functions source code"</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"timeout"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Time in seconds before the Lambda function will end with a failed state"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="mi">3</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_iam_role"</span> <span class="s2">"lambda_function"</span> <span class="p">{</span>
  <span class="nx">name</span>               <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">function_name</span>
  <span class="nx">assume_role_policy</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_iam_policy_document</span><span class="p">.</span><span class="nx">role</span><span class="p">.</span><span class="nx">json</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_iam_policy_document"</span> <span class="s2">"role"</span> <span class="p">{</span>
  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">principals</span> <span class="p">{</span>
      <span class="nx">type</span>        <span class="p">=</span> <span class="s2">"Service"</span>
      <span class="nx">identifiers</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"lambda.amazonaws.com"</span><span class="p">]</span>
    <span class="p">}</span>

    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"sts:AssumeRole"</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_iam_role_policy"</span> <span class="s2">"lambda_function"</span> <span class="p">{</span>
  <span class="nx">name</span>   <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">function_name</span>
  <span class="nx">role</span>   <span class="p">=</span> <span class="nx">aws_iam_role</span><span class="p">.</span><span class="nx">lambda_function</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">policy</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">iam_policy</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"random_uuid"</span> <span class="s2">"suffix"</span> <span class="p">{</span>
  <span class="nx">keepers</span> <span class="p">=</span> <span class="p">{</span>
    <span class="nx">for</span> <span class="nx">filename</span> <span class="nx">in</span> <span class="nx">fileset</span><span class="p">(</span><span class="kd">var</span><span class="p">.</span><span class="nx">source_dir</span><span class="p">,</span> <span class="s2">"**/*"</span><span class="p">)</span> <span class="err">:</span>
    <span class="nx">filename</span> <span class="p">=</span><span class="err">&gt;</span> <span class="nx">filemd5</span><span class="p">(</span><span class="s2">"</span><span class="k">${</span><span class="kd">var</span><span class="p">.</span><span class="nx">source_dir</span><span class="k">}</span><span class="s2">/</span><span class="k">${</span><span class="nx">filename</span><span class="k">}</span><span class="s2">"</span><span class="p">)</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"archive_file"</span> <span class="s2">"lambda_function"</span> <span class="p">{</span>
  <span class="nx">source_dir</span>  <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">source_dir</span>
  <span class="nx">output_path</span> <span class="p">=</span> <span class="s2">"</span><span class="k">${</span><span class="nx">path</span><span class="p">.</span><span class="k">module}</span><span class="s2">/lambda-function-</span><span class="k">${</span><span class="kd">var</span><span class="p">.</span><span class="nx">function_name</span><span class="k">}</span><span class="s2">-</span><span class="k">${</span><span class="nx">random_uuid</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">.zip"</span>
  <span class="nx">type</span>        <span class="p">=</span> <span class="s2">"zip"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_lambda_function"</span> <span class="s2">"lambda_function"</span> <span class="p">{</span>
  <span class="nx">function_name</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">function_name</span>
  <span class="nx">role</span>          <span class="p">=</span> <span class="nx">aws_iam_role</span><span class="p">.</span><span class="nx">lambda_function</span><span class="p">.</span><span class="nx">arn</span>
  <span class="nx">filename</span>      <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">archive_file</span><span class="p">.</span><span class="nx">lambda_function</span><span class="p">.</span><span class="nx">output_path</span>
  <span class="nx">runtime</span>       <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">runtime</span>
  <span class="nx">handler</span>       <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">handler</span>
  <span class="nx">timeout</span>       <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">timeout</span>

  <span class="nx">dynamic</span> <span class="s2">"environment"</span> <span class="p">{</span>
    <span class="nx">for_each</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">environment_variables</span> <span class="p">==</span> <span class="kc">null</span> <span class="err">?</span> <span class="p">[]</span> <span class="err">:</span> <span class="p">[</span><span class="mi">1</span><span class="p">]</span>
    <span class="nx">content</span> <span class="p">{</span>
      <span class="nx">variables</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">environment_variables</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">output</span> <span class="s2">"function_arn"</span> <span class="p">{</span>
  <span class="nx">value</span> <span class="p">=</span> <span class="nx">aws_lambda_function</span><span class="p">.</span><span class="nx">lambda_function</span><span class="p">.</span><span class="nx">arn</span>
<span class="p">}</span>

<span class="k">output</span> <span class="s2">"iam_role_arn"</span> <span class="p">{</span>
  <span class="nx">value</span> <span class="p">=</span> <span class="nx">aws_iam_role</span><span class="p">.</span><span class="nx">lambda_function</span><span class="p">.</span><span class="nx">arn</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This will zip the function code up, create an IAM role, and deploy the function in AWS.</p>

<h3 id="uploading-a-video">Uploading a Video</h3>
<p>Moving along to the actual ingress, we want an S3 bucket to old our videos. Back in the root of the repo:</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"random_string"</span> <span class="s2">"suffix"</span> <span class="p">{</span>
  <span class="nx">length</span>  <span class="p">=</span> <span class="mi">16</span>
  <span class="nx">special</span> <span class="p">=</span> <span class="kc">false</span>
  <span class="nx">upper</span>   <span class="p">=</span> <span class="kc">false</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_s3_bucket"</span> <span class="s2">"ingress"</span> <span class="p">{</span>
  <span class="nx">bucket</span> <span class="p">=</span> <span class="s2">"mediaconvert-test-ingress-</span><span class="k">${</span><span class="nx">random_string</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">"</span>
<span class="p">}</span>

<span class="k">output</span> <span class="s2">"ingress_bucket"</span> <span class="p">{</span>
  <span class="nx">value</span> <span class="p">=</span> <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">ingress</span><span class="p">.</span><span class="nx">bucket</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">random_string</code> will be appended to required resource names to ensure uniqueness.</p>

<p>Let’s create the the function and link it to the S3 bucket:</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_iam_policy_document"</span> <span class="s2">"ingress_processor_policy"</span> <span class="p">{</span>
  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
      <span class="s2">"logs:CreateLogGroup"</span><span class="p">,</span>
      <span class="s2">"logs:CreateLogStream"</span><span class="p">,</span>
      <span class="s2">"logs:PutLogEvents"</span>
    <span class="p">]</span>

    <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"*"</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">module</span> <span class="s2">"ingress_processor"</span> <span class="p">{</span>
  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"./modules/lambda_function"</span>

  <span class="nx">function_name</span> <span class="p">=</span> <span class="s2">"ingress-processor-</span><span class="k">${</span><span class="nx">random_string</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">handler</span>       <span class="p">=</span> <span class="s2">"lambda_function.lambda_handler"</span>
  <span class="nx">iam_policy</span>    <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_iam_policy_document</span><span class="p">.</span><span class="nx">ingress_processor_policy</span><span class="p">.</span><span class="nx">json</span>
  <span class="nx">runtime</span>       <span class="p">=</span> <span class="s2">"python3.7"</span>
  <span class="nx">source_dir</span>    <span class="p">=</span> <span class="s2">"</span><span class="k">${</span><span class="nx">path</span><span class="p">.</span><span class="nx">root</span><span class="k">}</span><span class="s2">/lambda_functions/ingress_processor"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_lambda_permission"</span> <span class="s2">"ingress_trigger"</span> <span class="p">{</span>
  <span class="nx">statement_id</span>  <span class="p">=</span> <span class="s2">"AllowExecutionFromS3Bucket"</span>
  <span class="nx">action</span>        <span class="p">=</span> <span class="s2">"lambda:InvokeFunction"</span>
  <span class="nx">function_name</span> <span class="p">=</span> <span class="k">module</span><span class="p">.</span><span class="nx">ingress_processor</span><span class="p">.</span><span class="nx">function_arn</span>
  <span class="nx">principal</span>     <span class="p">=</span> <span class="s2">"s3.amazonaws.com"</span>
  <span class="nx">source_arn</span>    <span class="p">=</span> <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">ingress</span><span class="p">.</span><span class="nx">arn</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_s3_bucket_notification"</span> <span class="s2">"ingress_trigger"</span> <span class="p">{</span>
  <span class="nx">bucket</span> <span class="p">=</span> <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">ingress</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">lambda_function</span> <span class="p">{</span>
    <span class="nx">lambda_function_arn</span> <span class="p">=</span> <span class="k">module</span><span class="p">.</span><span class="nx">ingress_processor</span><span class="p">.</span><span class="nx">function_arn</span>
    <span class="nx">events</span>              <span class="p">=</span> <span class="p">[</span><span class="s2">"s3:ObjectCreated:*"</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">depends_on</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_lambda_permission</span><span class="p">.</span><span class="nx">ingress_trigger</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And our Lambda function, <code class="language-plaintext highlighter-rouge">lambda_functions/ingress_processor/lambda_function.py</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">lambda_handler</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
</code></pre></div></div>

<p>At the moment our function doesn’t do much other than write the trigger event to CloudWatch so that we can see what we’re 
working with.</p>

<p>If you run <code class="language-plaintext highlighter-rouge">terraform apply</code> you’ll get a bucket name in the output. This is the bucket you want to upload videos to. Go ahead 
now and upload a test file. It doesn’t have to be anything special.</p>

<p>If you leave it sit for a while you’ll eventually see a message in the Lambda functions CloudWatch Logs.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>➜  aws s3 cp ingress.tf s3://mediaconvert-test-ingress-XXXXXX/video-id-here/video.mp4
upload: ./ingress.tf to s3://mediaconvert-test-ingress-XXXXXX/video-id-here/video.mp4
</code></pre></div></div>

<p>If you leave it sit for a while you’ll eventually see a message in the Lambda functions CloudWatch Logs.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"Records"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w">
      </span><span class="nl">"eventVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2.1"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"eventSource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"aws:s3"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"awsRegion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ap-southeast-2"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"eventTime"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2022-06-13T04:25:08.424Z"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"eventName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ObjectCreated:Put"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"userIdentity"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"principalId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AWS:XXXXXXX"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"requestParameters"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"sourceIPAddress"</span><span class="p">:</span><span class="w"> </span><span class="s2">"119.224.56.196"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"responseElements"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"x-amz-request-id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"XXXXX"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"x-amz-id-2"</span><span class="p">:</span><span class="w"> </span><span class="s2">"XXXXXX"</span><span class="w">
      </span><span class="p">},</span><span class="w">
      </span><span class="nl">"s3"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"s3SchemaVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"configurationId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"XXXXXXXX"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"bucket"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mediaconvert-test-ingress-XXXXXX"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"ownerIdentity"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="nl">"principalId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"XXXXXX"</span><span class="w">
          </span><span class="p">},</span><span class="w">
          </span><span class="nl">"arn"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:s3:::mediaconvert-test-ingress-XXXXXX"</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="nl">"object"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"key"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video-id-here/video.mp4"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"size"</span><span class="p">:</span><span class="w"> </span><span class="mi">1418</span><span class="p">,</span><span class="w">
          </span><span class="nl">"eTag"</span><span class="p">:</span><span class="w"> </span><span class="s2">"433ea94e91c11fae124bda110fab213b"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"sequencer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0062A6BC2452A2857F"</span><span class="w">
        </span><span class="p">}</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>The part we’re looking for is <code class="language-plaintext highlighter-rouge">Records[].s3.object.key</code>. This will tell us our filename.</p>

<h3 id="storing-video-state">Storing Video State</h3>
<p>Now that we can ingress a video we need to actually process it. But before that we’re going to need somewhere to store details 
about individual videos.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_dynamodb_table"</span> <span class="s2">"jobs"</span> <span class="p">{</span>
  <span class="nx">name</span>         <span class="p">=</span> <span class="s2">"jobs-</span><span class="k">${</span><span class="nx">random_string</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">billing_mode</span> <span class="p">=</span> <span class="s2">"PAY_PER_REQUEST"</span>

  <span class="nx">hash_key</span> <span class="p">=</span> <span class="s2">"VideoId"</span>

  <span class="nx">attribute</span> <span class="p">{</span>
    <span class="nx">name</span> <span class="p">=</span> <span class="s2">"VideoId"</span>
    <span class="nx">type</span> <span class="p">=</span> <span class="s2">"S"</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Add the following to <code class="language-plaintext highlighter-rouge">data.aws_iam_policy_document.ingress_processor_policy</code> to allow the function to read and write to the DB.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">statement</span> <span class="p">{</span>
  <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s2">"dynamodb:GetItem"</span><span class="p">,</span>
    <span class="s2">"dynamodb:BatchGetItem"</span><span class="p">,</span>
    <span class="s2">"dynamodb:Query"</span><span class="p">,</span>
    <span class="s2">"dynamodb:PutItem"</span><span class="p">,</span>
    <span class="s2">"dynamodb:UpdateItem"</span><span class="p">,</span>
    <span class="s2">"dynamodb:DeleteItem"</span>
  <span class="p">]</span>

  <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_dynamodb_table</span><span class="p">.</span><span class="nx">jobs</span><span class="p">.</span><span class="nx">arn</span><span class="p">]</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Set an environment variable in the <code class="language-plaintext highlighter-rouge">aws_lambda_function.ingress_processor</code></p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">module</span> <span class="s2">"ingress_processor"</span> <span class="p">{</span>
  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"./modules/lambda_function"</span>

  <span class="nx">function_name</span> <span class="p">=</span> <span class="s2">"ingress-processor-</span><span class="k">${</span><span class="nx">random_string</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">handler</span>       <span class="p">=</span> <span class="s2">"lambda_function.lambda_handler"</span>
  <span class="nx">iam_policy</span>    <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_iam_policy_document</span><span class="p">.</span><span class="nx">ingress_processor_policy</span><span class="p">.</span><span class="nx">json</span>
  <span class="nx">runtime</span>       <span class="p">=</span> <span class="s2">"python3.7"</span>
  <span class="nx">source_dir</span>    <span class="p">=</span> <span class="s2">"</span><span class="k">${</span><span class="nx">path</span><span class="p">.</span><span class="nx">root</span><span class="k">}</span><span class="s2">/lambda_functions/ingress_processor"</span>

  <span class="nx">environment_variables</span> <span class="p">=</span> <span class="p">{</span>
    <span class="s2">"JOBS_TABLE"</span> <span class="p">=</span> <span class="nx">aws_dynamodb_table</span><span class="p">.</span><span class="nx">jobs</span><span class="p">.</span><span class="nx">name</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Finally update the Ingress Lambda Function to save the job.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">boto3</span> 
<span class="kn">import</span> <span class="nn">os</span>


<span class="n">dynamodb</span> <span class="o">=</span> <span class="n">boto3</span><span class="p">.</span><span class="n">resource</span><span class="p">(</span><span class="s">'dynamodb'</span><span class="p">)</span>
<span class="n">table</span> <span class="o">=</span> <span class="n">dynamodb</span><span class="p">.</span><span class="n">Table</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'JOBS_TABLE'</span><span class="p">))</span>

<span class="k">def</span> <span class="nf">lambda_handler</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">record</span> <span class="ow">in</span> <span class="n">event</span><span class="p">[</span><span class="s">'Records'</span><span class="p">]:</span>
        <span class="n">filename</span> <span class="o">=</span> <span class="n">record</span><span class="p">[</span><span class="s">'s3'</span><span class="p">][</span><span class="s">'object'</span><span class="p">][</span><span class="s">'key'</span><span class="p">]</span>
        <span class="n">video_id</span> <span class="o">=</span> <span class="s">'/'</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">filename</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">'/'</span><span class="p">)[:</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>

        <span class="n">table</span><span class="p">.</span><span class="n">update_item</span><span class="p">(</span>
                <span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s">'VideoId'</span><span class="p">:</span> <span class="n">video_id</span><span class="p">},</span>
                <span class="n">UpdateExpression</span><span class="o">=</span><span class="s">'SET '</span> <span class="o">+</span>
                <span class="s">'IngressVideoBucket= :ingress_video_bucket,'</span> <span class="o">+</span>
                <span class="s">'IngressVideoKey= :ingress_video_key'</span><span class="p">,</span>
                <span class="n">ExpressionAttributeValues</span><span class="o">=</span><span class="p">{</span>
                    <span class="s">':ingress_video_bucket'</span><span class="p">:</span> <span class="n">record</span><span class="p">[</span><span class="s">'s3'</span><span class="p">][</span><span class="s">'bucket'</span><span class="p">][</span><span class="s">'name'</span><span class="p">],</span>
                    <span class="s">':ingress_video_key'</span><span class="p">:</span> <span class="n">filename</span>
                <span class="p">}</span>
        <span class="p">)</span>
</code></pre></div></div>

<p>And if we upload a new file, same as before, we can see it in Dynamo:</p>

<p><img src="/assets/posts/processing-video-with-mediaconvert-and-lambda/ingress-video-0001.png" /></p>

<h2 id="working-directories">Working Directories</h2>

<p>We need some S3 buckets before we can continue. A couple to store frames in and one to stash the output.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_s3_bucket"</span> <span class="s2">"raw_frames"</span> <span class="p">{</span>
  <span class="nx">bucket</span> <span class="p">=</span> <span class="s2">"mediaconvert-test-raw-frames-</span><span class="k">${</span><span class="nx">random_string</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_s3_bucket"</span> <span class="s2">"processed_frames"</span> <span class="p">{</span>
  <span class="nx">bucket</span> <span class="p">=</span> <span class="s2">"mediaconvert-test-processed-frames-</span><span class="k">${</span><span class="nx">random_string</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_s3_bucket"</span> <span class="s2">"egress"</span> <span class="p">{</span>
  <span class="nx">bucket</span> <span class="p">=</span> <span class="s2">"mediaconvert-test-egress-</span><span class="k">${</span><span class="nx">random_string</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">"</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="mediaconvert-iam-role">MediaConvert IAM Role</h2>

<p>Before we can kick off a MediaConvert job, we need an IAM role to grant it access to our resources.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_iam_role"</span> <span class="s2">"mediaconvert"</span> <span class="p">{</span>
  <span class="nx">name</span>               <span class="p">=</span> <span class="s2">"mediaconvert-</span><span class="k">${</span><span class="nx">random_string</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">assume_role_policy</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_iam_policy_document</span><span class="p">.</span><span class="nx">mediaconvert_role</span><span class="p">.</span><span class="nx">json</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_iam_role_policy"</span> <span class="s2">"mediaconvert"</span> <span class="p">{</span>
  <span class="nx">name</span>   <span class="p">=</span> <span class="nx">aws_iam_role</span><span class="p">.</span><span class="nx">mediaconvert</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">role</span>   <span class="p">=</span> <span class="nx">aws_iam_role</span><span class="p">.</span><span class="nx">mediaconvert</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">policy</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_iam_policy_document</span><span class="p">.</span><span class="nx">mediaconvert_policy</span><span class="p">.</span><span class="nx">json</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_iam_policy_document"</span> <span class="s2">"mediaconvert_role"</span> <span class="p">{</span>
  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">principals</span> <span class="p">{</span>
      <span class="nx">type</span>        <span class="p">=</span> <span class="s2">"Service"</span>
      <span class="nx">identifiers</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"mediaconvert.amazonaws.com"</span><span class="p">]</span>
    <span class="p">}</span>

    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"sts:AssumeRole"</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_iam_policy_document"</span> <span class="s2">"mediaconvert_policy"</span> <span class="p">{</span>
  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
      <span class="s2">"s3:Get*"</span><span class="p">,</span>
      <span class="s2">"s3:List*"</span>
    <span class="p">]</span>

    <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"</span><span class="k">${</span><span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">ingress</span><span class="p">.</span><span class="nx">arn</span><span class="k">}</span><span class="s2">/*"</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
      <span class="s2">"s3:Put*"</span>
    <span class="p">]</span>

    <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"</span><span class="k">${</span><span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">raw_frames</span><span class="p">.</span><span class="nx">arn</span><span class="k">}</span><span class="s2">/*"</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="creating-a-mediaconvert-job">Creating a MediaConvert Job</h2>

<p>MediaConvert jobs are submitted by a very long JSON file. Writing this file from scratch will suck, so we’re going to submit 
a job via the console and export the JSON.</p>

<p>Browse to the MediaConvert console and click on GetStarted.</p>

<p>Toss the URL of your test video sitting in S3.</p>

<p>On the left add an Output Group. This should be a File Group. Set the output S3 URL, this’ll be our raw frames bucket.</p>

<p>Open up the H.264 output and enter 5000000 in the Max bitrate.</p>

<p>Go back up to the File Group and click on Add Output.
Open up Output 2, Audio 1, and click on Remove Audio. 
Change the Container to “No container” and the Video Codec to “Frame Capture to JPEG”
Enter 30/1 in the Frame Rate.</p>

<p>Back to the left, click on AWS Integration. Ensure “Use an existing service role” is selected and paste in the ARN or the IAM 
role created in the previous step.</p>

<p>You should now be good to click on Create in the bottom right.</p>

<p>Once the job is complete, open it up and click on View JSON in the top right. We’re going to edit this a bit and save it into 
<code class="language-plaintext highlighter-rouge">lambda_functions/ingress_processor/mediaconvert_job.json.tpl</code></p>

<p>Remove everything wrapping around the <code class="language-plaintext highlighter-rouge">Settings</code> value until it is the root of the file. Replace the value of 
<code class="language-plaintext highlighter-rouge">OutputGroups.OutputGroupSettings.FileGroupSettings.Destination</code> with <code class="language-plaintext highlighter-rouge">${output_path}</code> and the value of <code class="language-plaintext highlighter-rouge">Inputs[0].FileInput</code> 
with <code class="language-plaintext highlighter-rouge">${input_path}</code>.</p>

<p>Back in our Ingress Lambda Function</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">boto3</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">import</span> <span class="nn">os</span>
<span class="kn">from</span> <span class="nn">string</span> <span class="kn">import</span> <span class="n">Template</span>


<span class="n">dynamodb</span> <span class="o">=</span> <span class="n">boto3</span><span class="p">.</span><span class="n">resource</span><span class="p">(</span><span class="s">'dynamodb'</span><span class="p">)</span>
<span class="n">mediaconvert</span> <span class="o">=</span> <span class="n">boto3</span><span class="p">.</span><span class="n">client</span><span class="p">(</span><span class="s">'mediaconvert'</span><span class="p">)</span>

<span class="n">mediaconvert_endpoints</span> <span class="o">=</span> <span class="n">mediaconvert</span><span class="p">.</span><span class="n">describe_endpoints</span><span class="p">()</span>
<span class="n">mediaconvert</span> <span class="o">=</span> <span class="n">boto3</span><span class="p">.</span><span class="n">client</span><span class="p">(</span><span class="s">'mediaconvert'</span><span class="p">,</span> <span class="n">endpoint_url</span><span class="o">=</span><span class="n">mediaconvert_endpoints</span><span class="p">[</span><span class="s">'Endpoints'</span><span class="p">][</span><span class="mi">0</span><span class="p">][</span><span class="s">'Url'</span><span class="p">])</span>

<span class="n">table</span> <span class="o">=</span> <span class="n">dynamodb</span><span class="p">.</span><span class="n">Table</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'JOBS_TABLE'</span><span class="p">))</span>

<span class="n">frames_bucket</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'FRAMES_BUCKET'</span><span class="p">)</span>
<span class="n">mediaconvert_role</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'MEDIACONVERT_ROLE'</span><span class="p">)</span>


<span class="k">def</span> <span class="nf">lambda_handler</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">record</span> <span class="ow">in</span> <span class="n">event</span><span class="p">[</span><span class="s">'Records'</span><span class="p">]:</span>
        <span class="n">filepath</span> <span class="o">=</span> <span class="n">record</span><span class="p">[</span><span class="s">'s3'</span><span class="p">][</span><span class="s">'object'</span><span class="p">][</span><span class="s">'key'</span><span class="p">]</span>
        <span class="n">video_id</span> <span class="o">=</span> <span class="s">'/'</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">filepath</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">'/'</span><span class="p">)[:</span><span class="o">-</span><span class="mi">1</span><span class="p">])</span>
        <span class="n">input_bucket</span> <span class="o">=</span> <span class="n">record</span><span class="p">[</span><span class="s">'s3'</span><span class="p">][</span><span class="s">'bucket'</span><span class="p">][</span><span class="s">'name'</span><span class="p">]</span>

        <span class="n">input_path</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'s3://</span><span class="si">{</span><span class="n">input_bucket</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">filepath</span><span class="si">}</span><span class="s">'</span>
        <span class="n">output_path</span> <span class="o">=</span> <span class="sa">f</span><span class="s">'s3://</span><span class="si">{</span><span class="n">frames_bucket</span><span class="si">}</span><span class="s">/</span><span class="si">{</span><span class="n">video_id</span><span class="si">}</span><span class="s">/'</span>        

        <span class="n">job_config</span> <span class="o">=</span> <span class="p">{</span>
            <span class="s">'input_path'</span><span class="p">:</span> <span class="n">input_path</span><span class="p">,</span>
            <span class="s">'output_path'</span><span class="p">:</span> <span class="n">output_path</span>
        <span class="p">}</span>
        
        <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">'./mediaconvert_job.json.tpl'</span><span class="p">,</span> <span class="s">'r'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="n">src</span> <span class="o">=</span> <span class="n">Template</span><span class="p">(</span><span class="n">f</span><span class="p">.</span><span class="n">read</span><span class="p">()).</span><span class="n">substitute</span><span class="p">(</span><span class="n">job_config</span><span class="p">)</span>
            <span class="n">job</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">src</span><span class="p">)</span>
        
        <span class="n">res</span> <span class="o">=</span> <span class="n">mediaconvert</span><span class="p">.</span><span class="n">create_job</span><span class="p">(</span>
            <span class="n">Role</span><span class="o">=</span><span class="n">mediaconvert_role</span><span class="p">,</span> 
            <span class="n">Settings</span><span class="o">=</span><span class="n">job</span><span class="p">,</span>
            <span class="n">UserMetadata</span><span class="o">=</span><span class="p">{</span>
                <span class="s">'videoId'</span><span class="p">:</span> <span class="n">video_id</span><span class="p">,</span>
                <span class="s">'task'</span><span class="p">:</span> <span class="s">'GENERATE_FRAMES'</span>
            <span class="p">}</span>
        <span class="p">)</span>
        <span class="n">mediaconvert_job_id</span> <span class="o">=</span> <span class="n">res</span><span class="p">[</span><span class="s">'Job'</span><span class="p">][</span><span class="s">'Id'</span><span class="p">]</span>

        <span class="n">table</span><span class="p">.</span><span class="n">update_item</span><span class="p">(</span>
                <span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s">'VideoId'</span><span class="p">:</span> <span class="n">video_id</span><span class="p">},</span>
                <span class="n">UpdateExpression</span><span class="o">=</span><span class="s">'SET '</span> <span class="o">+</span>
                <span class="s">'IngressVideoBucket= :ingress_video_bucket,'</span> <span class="o">+</span>
                <span class="s">'IngressVideoKey= :ingress_video_key,'</span> <span class="o">+</span>
                <span class="s">'MediaConvertJobId= :mediaconvert_job_id'</span><span class="p">,</span>
                <span class="n">ExpressionAttributeValues</span><span class="o">=</span><span class="p">{</span>
                    <span class="s">':ingress_video_bucket'</span><span class="p">:</span> <span class="n">record</span><span class="p">[</span><span class="s">'s3'</span><span class="p">][</span><span class="s">'bucket'</span><span class="p">][</span><span class="s">'name'</span><span class="p">],</span>
                    <span class="s">':ingress_video_key'</span><span class="p">:</span> <span class="n">filepath</span><span class="p">,</span>
                    <span class="s">':mediaconvert_job_id'</span><span class="p">:</span> <span class="n">mediaconvert_job_id</span>
                <span class="p">}</span>
        <span class="p">)</span>
</code></pre></div></div>

<p>And finally update our Terraform to add the additional environment variables and permissions.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">data</span> <span class="s2">"aws_iam_policy_document"</span> <span class="s2">"ingress_processor_policy"</span> <span class="p">{</span>
  <span class="p">...</span>

  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
      <span class="s2">"mediaconvert:CreateJob"</span><span class="p">,</span>
      <span class="s2">"mediaconvert:DescribeEndpoints"</span>
    <span class="p">]</span>

    <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"*"</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
      <span class="s2">"iam:PassRole"</span>
    <span class="p">]</span>

    <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="nx">aws_iam_role</span><span class="p">.</span><span class="nx">mediaconvert</span><span class="p">.</span><span class="nx">arn</span><span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">module</span> <span class="s2">"ingress_processor"</span> <span class="p">{</span>
  <span class="p">...</span>

  <span class="nx">environment_variables</span> <span class="p">=</span> <span class="p">{</span>
    <span class="s2">"FRAMES_BUCKET"</span>     <span class="p">=</span> <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">raw_frames</span><span class="p">.</span><span class="nx">bucket</span>
    <span class="s2">"JOBS_TABLE"</span>        <span class="p">=</span> <span class="nx">aws_dynamodb_table</span><span class="p">.</span><span class="nx">jobs</span><span class="p">.</span><span class="nx">name</span>
    <span class="s2">"MEDIACONVERT_ROLE"</span> <span class="p">=</span> <span class="nx">aws_iam_role</span><span class="p">.</span><span class="nx">mediaconvert</span><span class="p">.</span><span class="nx">arn</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>If you kick off a video now, you’ll see some output getting spit out into the Frames bucket.</p>

<h2 id="handling-mediaconvert-success">Handling MediaConvert Success</h2>

<p>One thing we need to now is how manu frames we’re going to end up with so we can keep track of progress. Unfortunately, due to 
how we’re extracting them, there’s no easy way to get this information out of the video in advance. So what we need to do is 
count the number of frames in the S3 bucket after MediaConvert has finished. This’ll be done via a Lambda Function we’re going 
to Trigger off an EventBridge event.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">resource</span> <span class="s2">"aws_cloudwatch_event_rule"</span> <span class="s2">"mediaconvert_success"</span> <span class="p">{</span>
  <span class="nx">name</span>        <span class="p">=</span> <span class="s2">"mediaconvert-success-</span><span class="k">${</span><span class="nx">random_string</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Trigger for when a MediaConvert job finishes"</span>

  <span class="nx">event_pattern</span> <span class="p">=</span> <span class="o">&lt;&lt;-</span><span class="no">EOF</span><span class="sh">
    {
      "source": [
        "aws.mediaconvert"
      ],
      "detail-type": [
        "MediaConvert Job State Change"
      ],
      "detail": {
        "status": [
          "COMPLETE"
        ]
      }
    }
</span><span class="no">  EOF 
</span><span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_cloudwatch_event_target"</span> <span class="s2">"mediaconvert_success_handler"</span> <span class="p">{</span>
  <span class="nx">rule</span>      <span class="p">=</span> <span class="nx">aws_cloudwatch_event_rule</span><span class="p">.</span><span class="nx">mediaconvert_success</span><span class="p">.</span><span class="nx">name</span>
  <span class="nx">target_id</span> <span class="p">=</span> <span class="s2">"LambdaFunction"</span>
  <span class="nx">arn</span>       <span class="p">=</span> <span class="k">module</span><span class="p">.</span><span class="nx">mediaconvert_success_handler</span><span class="p">.</span><span class="nx">function_arn</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_lambda_permission"</span> <span class="s2">"mediaconvert_success_handler"</span> <span class="p">{</span>
  <span class="nx">statement_id</span>  <span class="p">=</span> <span class="s2">"AllowExecutionFromEventBridge"</span>
  <span class="nx">action</span>        <span class="p">=</span> <span class="s2">"lambda:InvokeFunction"</span>
  <span class="nx">function_name</span> <span class="p">=</span> <span class="k">module</span><span class="p">.</span><span class="nx">mediaconvert_success_handler</span><span class="p">.</span><span class="nx">function_arn</span>
  <span class="nx">principal</span>     <span class="p">=</span> <span class="s2">"events.amazonaws.com"</span>
  <span class="nx">source_arn</span>    <span class="p">=</span> <span class="nx">aws_cloudwatch_event_rule</span><span class="p">.</span><span class="nx">mediaconvert_success</span><span class="p">.</span><span class="nx">arn</span>
<span class="p">}</span>

<span class="k">module</span> <span class="s2">"mediaconvert_success_handler"</span> <span class="p">{</span>
  <span class="nx">source</span> <span class="p">=</span> <span class="s2">"./modules/lambda_function"</span>

  <span class="nx">function_name</span> <span class="p">=</span> <span class="s2">"mediaconvert-success-handler-</span><span class="k">${</span><span class="nx">random_string</span><span class="p">.</span><span class="nx">suffix</span><span class="p">.</span><span class="nx">result</span><span class="k">}</span><span class="s2">"</span>
  <span class="nx">handler</span>       <span class="p">=</span> <span class="s2">"lambda_function.lambda_handler"</span>
  <span class="nx">iam_policy</span>    <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_iam_policy_document</span><span class="p">.</span><span class="nx">mediaconvert_success_handler</span><span class="p">.</span><span class="nx">json</span>
  <span class="nx">runtime</span>       <span class="p">=</span> <span class="s2">"python3.8"</span>
  <span class="nx">source_dir</span>    <span class="p">=</span> <span class="s2">"</span><span class="k">${</span><span class="nx">path</span><span class="p">.</span><span class="nx">root</span><span class="k">}</span><span class="s2">/lambda_functions/mediaconvert_success_handler"</span>
  <span class="nx">timeout</span>       <span class="p">=</span> <span class="mi">60</span> <span class="c1"># This may need to be adjusted depending on max video length and framerate</span>

  <span class="nx">environment_variables</span> <span class="p">=</span> <span class="p">{</span>
    <span class="s2">"JOBS_TABLE"</span>              <span class="p">=</span> <span class="nx">aws_dynamodb_table</span><span class="p">.</span><span class="nx">jobs</span><span class="p">.</span><span class="nx">name</span>
    <span class="s2">"PROCESSED_FRAMES_BUCKET"</span> <span class="p">=</span> <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">processed_frames</span><span class="p">.</span><span class="nx">bucket</span>
    <span class="s2">"RAW_FRAMES_BUCKET"</span>       <span class="p">=</span> <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">raw_frames</span><span class="p">.</span><span class="nx">bucket</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_iam_policy_document"</span> <span class="s2">"mediaconvert_success_handler"</span> <span class="p">{</span>
  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
      <span class="s2">"logs:CreateLogGroup"</span><span class="p">,</span>
      <span class="s2">"logs:CreateLogStream"</span><span class="p">,</span>
      <span class="s2">"logs:PutLogEvents"</span>
    <span class="p">]</span>

    <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"*"</span><span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
      <span class="s2">"s3:ListBucket"</span>
    <span class="p">]</span>

    <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span>
      <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">raw_frames</span><span class="p">.</span><span class="nx">arn</span><span class="p">,</span>
      <span class="nx">aws_s3_bucket</span><span class="p">.</span><span class="nx">processed_frames</span><span class="p">.</span><span class="nx">arn</span>
    <span class="p">]</span>
  <span class="p">}</span>

  <span class="nx">statement</span> <span class="p">{</span>
    <span class="nx">actions</span> <span class="p">=</span> <span class="p">[</span>
      <span class="s2">"dynamodb:GetItem"</span><span class="p">,</span>
      <span class="s2">"dynamodb:UpdateItem"</span>
    <span class="p">]</span>

    <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span>
      <span class="nx">aws_dynamodb_table</span><span class="p">.</span><span class="nx">jobs</span><span class="p">.</span><span class="nx">arn</span>
    <span class="p">]</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Our input message looks like this</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"8e35a40e-b5f3-cfac-4688-72345559362d"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"detail-type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"MediaConvert Job State Change"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"aws.mediaconvert"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"account"</span><span class="p">:</span><span class="w"> </span><span class="s2">"XXXX"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"time"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2022-07-03T04:30:29Z"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"region"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ap-southeast-2"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"resources"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="s2">"arn:aws:mediaconvert:ap-southeast-2:XXXX:jobs/1656822614416-ocyvn0"</span><span class="w">
  </span><span class="p">],</span><span class="w">
  </span><span class="nl">"detail"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"timestamp"</span><span class="p">:</span><span class="w"> </span><span class="mi">1656822629927</span><span class="p">,</span><span class="w">
    </span><span class="nl">"accountId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"XXXX"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"queue"</span><span class="p">:</span><span class="w"> </span><span class="s2">"arn:aws:mediaconvert:ap-southeast-2:XXXX:queues/Default"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"jobId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1656822614416-ocyvn0"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"COMPLETE"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"userMetadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"videoId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"41becbca-1fd9-460b-b2b4-9c9470dc6c21"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"task"</span><span class="p">:</span><span class="w"> </span><span class="s2">"GENERATE_FRAMES"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"outputGroupDetails"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="nl">"outputDetails"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
          </span><span class="p">{</span><span class="w">
            </span><span class="nl">"outputFilePaths"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
              </span><span class="s2">"s3://mediaconvert-test-raw-frames-8m3yofu4o8fhw145/41becbca-1fd9-460b-b2b4-9c9470dc6c21/video.mp4"</span><span class="w">
            </span><span class="p">],</span><span class="w">
            </span><span class="nl">"durationInMs"</span><span class="p">:</span><span class="w"> </span><span class="mi">31664</span><span class="p">,</span><span class="w">
            </span><span class="nl">"videoDetails"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
              </span><span class="nl">"widthInPx"</span><span class="p">:</span><span class="w"> </span><span class="mi">1280</span><span class="p">,</span><span class="w">
              </span><span class="nl">"heightInPx"</span><span class="p">:</span><span class="w"> </span><span class="mi">720</span><span class="w">
            </span><span class="p">}</span><span class="w">
          </span><span class="p">},</span><span class="w">
          </span><span class="p">{</span><span class="w">
            </span><span class="nl">"outputFilePaths"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
              </span><span class="s2">"s3://mediaconvert-test-raw-frames-8m3yofu4o8fhw145/41becbca-1fd9-460b-b2b4-9c9470dc6c21/video.0000949.jpg"</span><span class="w">
            </span><span class="p">],</span><span class="w">
            </span><span class="nl">"durationInMs"</span><span class="p">:</span><span class="w"> </span><span class="mi">31666</span><span class="p">,</span><span class="w">
            </span><span class="nl">"videoDetails"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
              </span><span class="nl">"widthInPx"</span><span class="p">:</span><span class="w"> </span><span class="mi">1280</span><span class="p">,</span><span class="w">
              </span><span class="nl">"heightInPx"</span><span class="p">:</span><span class="w"> </span><span class="mi">720</span><span class="w">
            </span><span class="p">}</span><span class="w">
          </span><span class="p">}</span><span class="w">
        </span><span class="p">],</span><span class="w">
        </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"FILE_GROUP"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">]</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>We can get the Video ID from one of the output paths. In <code class="language-plaintext highlighter-rouge">lambda_functions/mediaconvert_success_handler/lambda_function.py</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">boto3</span>
<span class="kn">import</span> <span class="nn">os</span>

<span class="n">dynamodb</span> <span class="o">=</span> <span class="n">boto3</span><span class="p">.</span><span class="n">resource</span><span class="p">(</span><span class="s">'dynamodb'</span><span class="p">)</span>
<span class="n">s3</span> <span class="o">=</span> <span class="n">boto3</span><span class="p">.</span><span class="n">client</span><span class="p">(</span><span class="s">'s3'</span><span class="p">)</span>

<span class="n">table</span> <span class="o">=</span> <span class="n">dynamodb</span><span class="p">.</span><span class="n">Table</span><span class="p">(</span><span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'JOBS_TABLE'</span><span class="p">))</span>
<span class="n">raw_frames_bucket</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'RAW_FRAMES_BUCKET'</span><span class="p">)</span>
<span class="n">processed_frames_bucket</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">environ</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'PROCESSED_FRAMES_BUCKET'</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">get_s3_key_count</span><span class="p">(</span><span class="n">bucket_name</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">continue_token</span><span class="o">=</span><span class="bp">None</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">continue_token</span> <span class="o">!=</span> <span class="bp">None</span><span class="p">:</span>
        <span class="n">s3_res</span> <span class="o">=</span> <span class="n">s3</span><span class="p">.</span><span class="n">list_objects_v2</span><span class="p">(</span>
                <span class="n">Bucket</span><span class="o">=</span><span class="n">bucket_name</span><span class="p">,</span>
                <span class="n">Prefix</span><span class="o">=</span><span class="n">path</span><span class="p">,</span>
                <span class="n">ContinuationToken</span><span class="o">=</span><span class="n">continue_token</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="n">s3_res</span> <span class="o">=</span> <span class="n">s3</span><span class="p">.</span><span class="n">list_objects_v2</span><span class="p">(</span>
                <span class="n">Bucket</span><span class="o">=</span><span class="n">bucket_name</span><span class="p">,</span>
                <span class="n">Prefix</span><span class="o">=</span><span class="n">path</span><span class="p">)</span>

    <span class="n">count</span> <span class="o">=</span> <span class="n">s3_res</span><span class="p">[</span><span class="s">'KeyCount'</span><span class="p">]</span>

    <span class="k">if</span> <span class="n">s3_res</span><span class="p">[</span><span class="s">'IsTruncated'</span><span class="p">]:</span>
        <span class="n">count</span> <span class="o">+=</span> <span class="n">get_s3_key_count</span><span class="p">(</span><span class="n">bucket_name</span><span class="p">,</span> <span class="n">path</span><span class="p">,</span> <span class="n">s3_res</span><span class="p">[</span><span class="s">'NextContinuationToken'</span><span class="p">])</span>

    <span class="k">return</span> <span class="n">count</span>


<span class="k">def</span> <span class="nf">video_split_handler</span><span class="p">(</span><span class="n">event</span><span class="p">):</span>
    <span class="n">video_id</span> <span class="o">=</span> <span class="n">event</span><span class="p">[</span><span class="s">'detail'</span><span class="p">][</span><span class="s">'userMetadata'</span><span class="p">][</span><span class="s">'videoId'</span><span class="p">]</span>
    <span class="n">mediaconvert_status</span> <span class="o">=</span> <span class="n">event</span><span class="p">[</span><span class="s">'detail'</span><span class="p">][</span><span class="s">'status'</span><span class="p">]</span>

    <span class="n">total_frames</span> <span class="o">=</span> <span class="n">get_s3_key_count</span><span class="p">(</span><span class="n">raw_frames_bucket</span><span class="p">,</span> <span class="n">video_id</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span> <span class="c1"># subtracting the video file 
</span>
    <span class="n">table</span><span class="p">.</span><span class="n">update_item</span><span class="p">(</span>
            <span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s">'VideoId'</span><span class="p">:</span> <span class="n">video_id</span><span class="p">},</span>
            <span class="n">UpdateExpression</span><span class="o">=</span><span class="s">'SET MediaConvertStatus= :mediaconvertStatus, TotalFrames= :totalFrames'</span><span class="p">,</span>
            <span class="n">ExpressionAttributeValues</span><span class="o">=</span><span class="p">{</span><span class="s">':mediaconvertStatus'</span><span class="p">:</span> <span class="n">mediaconvert_status</span><span class="p">,</span> <span class="s">':totalFrames'</span><span class="p">:</span> <span class="n">total_frames</span><span class="p">}</span>
    <span class="p">)</span>


<span class="k">def</span> <span class="nf">lambda_handler</span><span class="p">(</span><span class="n">event</span><span class="p">,</span> <span class="n">context</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">event</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">'detail'</span><span class="p">,</span> <span class="p">{}).</span><span class="n">get</span><span class="p">(</span><span class="s">'userMetadata'</span><span class="p">,</span> <span class="p">{}).</span><span class="n">get</span><span class="p">(</span><span class="s">'task'</span><span class="p">)</span> <span class="o">==</span> <span class="s">'GENERATE_FRAMES'</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">video_split_handler</span><span class="p">(</span><span class="n">event</span><span class="p">)</span>
</code></pre></div></div>

<p>From here, toss a video into the ingress bucket (remember to include a video ID directory) and watch it get ripped to pieces.</p>

<h2 id="wrapping-up">Wrapping Up</h2>

<p>With that we have an S3 bucket we can toss videos into. They’ll automatically get picked up and run through MediaConvert. 
Individual frames will be dumped out into another S3 bucket, from which they can be processed further.</p>

<p>The biggest takeaway for me was to never just skip over a potential solution simply because it doesn’t look to be designed for
your purpose. Everything is a hammer if you’re game enough.</p>

<p>If you’re having difficulty following allow, you can find a working copy on my 
<a href="https://github.com/incpac/mediaconver-frame-processor/releases/tag/blogpost%2Fprocessing-video-mediaconvert-lambda">GitHub.</a></p>]]></content><author><name>Thomas Claridge</name></author><summary type="html"><![CDATA[Story Time: So one day I’m just sitting at my desk and my manager gets all up in my business with “Yo, you with the face. Come with me.”]]></summary></entry><entry><title type="html">AWS Direct Connect with Terraform</title><link href="https://incpac.github.io/posts/2022-05-27-aws-direct-connect-with-terraform/" rel="alternate" type="text/html" title="AWS Direct Connect with Terraform" /><published>2022-05-27T00:00:00+12:00</published><updated>2022-05-27T00:00:00+12:00</updated><id>https://incpac.github.io/posts/aws-direct-connect-with-terraform</id><content type="html" xml:base="https://incpac.github.io/posts/2022-05-27-aws-direct-connect-with-terraform/"><![CDATA[<p>If you’ve never been to one, <a href="https://aws.amazon.com/partners/immersion-days/">Immersion Days</a> are great. Generally there’ll be 
a presentation about the techin question, then some hands on labs letting you get dirty with it. I really recommend 
participating if one comes your way.</p>

<p>For as long as I’ve been working for him, my boss has been repeatedly <em>(read once or twice)</em> mentioned that he’s been harrassing 
AWS to provide an Immersion Day for Direct Connect.</p>

<p><a href="https://aws.amazon.com/directconnect/">Direct Connect</a> is a direct network connect between your office or data center and an 
AWS region. This lets you get dedicated bandwidth on a private connection that exists outside of the internet.</p>

<p>As you can imagine creating a lab environment where you need to be able to create and destroy dedicated WAN connections on a
whim is a bit hard. Regardless, Amazon have 
<a href="https://catalog.us-east-1.prod.workshops.aws/workshops/5a1ba4bb-e16a-4e40-9fdb-6c1925e73a73/en-US">come through</a> and it was 
awesome. It was great being able to get some hands-on experience and having an instructor walk you through the 
process was invaluable.</p>

<p>Immersion Day lab environments tend to hang around for a few hours after the days has finished and this one was no exception. 
So after everything’s wrapped up, I tore it all down and hit up the Terraform docs.</p>

<h1 id="its-business-time">It’s Business Time</h1>

<p>To start with, I really recommend reading through the 
<a href="https://catalog.us-east-1.prod.workshops.aws/workshops/5a1ba4bb-e16a-4e40-9fdb-6c1925e73a73/en-US/2-establishing-connectivity">Provision Direct Connect Connectivity</a> 
part of the Immersion Day guide. This is what we’re going to be recreating with Terraform and it’ll help provide a visual 
overview of what you’re doing.</p>

<p>If you’ve read the guide, you’ll have seend that, our existing environment cosists of multiple VPCs and a Transit Gateway. Our 
example has 10.0.0.0/24 as the on-prem network and 172.16.0.0./16 in AWS.</p>

<p>First thing you’re going to do is go to your Direct Connect provider and request a connection. The specifics of this are outside 
the scope of this post. Eventually you’ll see the connection in your AWS console where you can accept it.</p>

<p><img src="/assets/posts/aws-direct-connect-with-terraform/dx-connection-ordering.png" /></p>

<p>Next we want a Direct Connect Gateway. This requires a name and an ASN for the Amazon side. We’re going to use 64512 and stash it
in a variable.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">variable</span> <span class="s2">"aws_side_bgp_asn"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"BPG ASN used on the AWS side"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="mi">64512</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_dx_gateway"</span> <span class="s2">"dx"</span> <span class="p">{</span>
  <span class="nx">name</span>            <span class="p">=</span> <span class="s2">"direct-connect-lab"</span>
  <span class="nx">amazon_side_asn</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">aws_side_bgp_asn</span>
<span class="p">}</span>
</code></pre></div></div>

<p>We now need to associate the DX Gateway with our pre-existing Transit Gateway. Here we’ll need the ID of the Transit Gateway and 
the CIDR ranges for all VPCs attached to it. This can be one large, all encompassing block if desired.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">variable</span> <span class="s2">"transit_gateway_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"ID of the pre-existing Transit Gateway to attach the DX to"</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"transit_gateway_ip_prefixes"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Subnets attached to the Transit Gateway to allow to route through the DX conn"</span>
  <span class="nx">default</span> <span class="p">=</span> <span class="p">[</span>
    <span class="s2">"172.16.0.0/16"</span>
  <span class="p">]</span>
<span class="p">}</span>

<span class="k">data</span> <span class="s2">"aws_ec2_transit_gateway"</span> <span class="s2">"example"</span> <span class="p">{</span>
  <span class="nx">id</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">transit_gateway_id</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_dx_gateway_association"</span> <span class="s2">"dx"</span> <span class="p">{</span>
  <span class="nx">dx_gateway_id</span>         <span class="p">=</span> <span class="nx">aws_dx_gateway</span><span class="p">.</span><span class="nx">dx</span><span class="p">.</span><span class="nx">id</span>
  <span class="nx">associated_gateway_id</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_ec2_transit_gateway</span><span class="p">.</span><span class="nx">example</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">allowed_prefixes</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">transit_gateway_ip_prefixes</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">transit_gateway_ip_prefixes</code> is a list of subnets attached to the Trasnit Gateway. These get advertised to the DX Gateway.</p>

<p>Next we create a Transit Virtual Interface and provide it the IP addresses of the Amazon and customer sides of the connection. 
It will also need a BGP auth key.</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">variable</span> <span class="s2">"direct_connect_connection_id"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"ID of the DX Connection created by the DX provider"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="s2">"dxcon-fg5dryvu"</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"vlan"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"The VLAN used by the DX Connection. Provided by DX provider"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="mi">222</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"dx_gateway_bgp_asn"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"BGP ASN used by the DX Gateway"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="mi">64513</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"dx_amazon_address"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"IP Address of the Amazon side of the DX connection"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="s2">"169.254.100.2/30"</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"dx_customer_address"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"IP Address of the customer side of the DX connection"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="s2">"169.254.100.1/30"</span>
<span class="p">}</span>

<span class="k">variable</span> <span class="s2">"dx_bgp_auth_key"</span> <span class="p">{</span>
  <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Key used for the BGP authentication"</span>
  <span class="nx">default</span>     <span class="p">=</span> <span class="s2">"badPassword"</span>
<span class="p">}</span>

<span class="k">resource</span> <span class="s2">"aws_dx_transit_virtual_interface"</span> <span class="s2">"dx"</span> <span class="p">{</span>
  <span class="nx">name</span> <span class="p">=</span> <span class="s2">"direct-connect-lab"</span>

  <span class="nx">connection_id</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">direct_connect_connection_id</span>
  <span class="nx">dx_gateway_id</span> <span class="p">=</span> <span class="nx">aws_dx_gateway</span><span class="p">.</span><span class="nx">dx</span><span class="p">.</span><span class="nx">id</span>

  <span class="nx">vlan</span>           <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">vlan</span>
  <span class="nx">address_family</span> <span class="p">=</span> <span class="s2">"ipv4"</span>
  <span class="nx">bgp_asn</span>        <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">dx_gateway_bgp_asn</span>

  <span class="nx">amazon_address</span>   <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">dx_amazon_address</span>
  <span class="nx">customer_address</span> <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">dx_customer_address</span>
  <span class="nx">bgp_auth_key</span>     <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">dx_bgp_auth_key</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Your router will need configuring. The following is an example for the virtual router provided in the lab. The specifics for 
yours will vary.</p>

<p>Setting the Router IP:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>conf t
interface eth1
ip address 169.254.100.1/30
exit
exit
show interace brief
</code></pre></div></div>

<p>Configuring BGP:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>conf t
route bgp 64513
neighbor 169.254.100.2 remote-as 64512
neighbor 169.254.100.2 password badPassword
address-family ipv4 unicast
network 10.0.0.0/24
exit
exit
exit
show bgp detail
</code></pre></div></div>

<p>With all that we should be good to deploy the Terraform.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>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:

  # aws_dx_gateway.dx will be created
  + resource "aws_dx_gateway" "dx" {
      + amazon_side_asn  = "64512"
      + id               = (known after apply)
      + name             = "direct-connect-lab"
      + owner_account_id = (known after apply)
    }

  # aws_dx_gateway_association.dx will be created
  + resource "aws_dx_gateway_association" "dx" {
      + allowed_prefixes                    = [
          + "172.16.0.0/16",
        ]
      + associated_gateway_id               = "tgw-0dffb943a3221b366"
      + associated_gateway_owner_account_id = (known after apply)
      + associated_gateway_type             = (known after apply)
      + dx_gateway_association_id           = (known after apply)
      + dx_gateway_id                       = (known after apply)
      + dx_gateway_owner_account_id         = (known after apply)
      + id                                  = (known after apply)
    }

  # aws_dx_transit_virtual_interface.dx will be created
  + resource "aws_dx_transit_virtual_interface" "dx" {
      + address_family      = "ipv4"
      + amazon_address      = "169.254.100.2/30"
      + amazon_side_asn     = (known after apply)
      + arn                 = (known after apply)
      + aws_device          = (known after apply)
      + bgp_asn             = 64513
      + bgp_auth_key        = "badPassword"
      + connection_id       = "dxcon-fg5dryvu"
      + customer_address    = "169.254.100.1/30"
      + dx_gateway_id       = (known after apply)
      + id                  = (known after apply)
      + jumbo_frame_capable = (known after apply)
      + mtu                 = 1500
      + name                = "direct-connect-lab"
      + tags_all            = (known after apply)
      + vlan                = 222
    }

Plan: 3 to add, 0 to change, 0 to destroy.
</code></pre></div></div>

<p>Once that’s deployed we can connect to an EC2 instance and hit the on-prem network. In this case we’ll log into the server at 
10.0.0.253.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>sh-4.2$ ssh labuser@10.0.0.253
labuser@10.0.0.253's password:
Last login: Wed May 18 08:12:07 2022 from 172.16.94.57

[labuser@dns ~]$ ip addr
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
991: eth0@if992: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether f2:e1:12:74:d9:d8 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.0.0.253/32 brd 10.0.0.253 scope global eth0
       valid_lft forever preferred_lft forever
    inet6 fe80::f0e1:12ff:fe74:d9d8/64 scope link
       valid_lft forever preferred_lft forever
993: eth1@if994: &lt;BROADCAST,MULTICAST&gt; mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 9a:30:45:10:60:12 brd ff:ff:ff:ff:ff:ff link-netnsid 0
995: eth2@if996: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 96:18:e7:75:7f:f6 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::9418:e7ff:fe75:7ff6/64 scope link
       valid_lft forever preferred_lft forever
</code></pre></div></div>

<h1 id="random-unstructured-closing-thoughts">Random, Unstructured Closing Thoughts</h1>

<p>The Terraform stack deploys a lot faster than you’d think. This was especially helpful as the primary on-prem server I was 
testing the connection with had crashed. I tore the connection down and stood it up again a few times until I noticed that it
was the on-prem side reporting that the ping failed, and switching to a dffirent on-prem server proved that the connection 
worked.</p>

<p>You can accept the DX connection from the vendor using a <code class="language-plaintext highlighter-rouge">aws_dx_connection_confirmation</code> TF resource, however this one feels a 
bit iffy. Personally, I’d confirm the connection manually, then create the other required resources with Terraform.</p>

<p>The Immersion Day was really fun. Having the lab around afterwards was great for being able to test different deployments.</p>]]></content><author><name>Thomas Claridge</name></author><summary type="html"><![CDATA[If you’ve never been to one, Immersion Days are great. Generally there’ll be a presentation about the techin question, then some hands on labs letting you get dirty with it. I really recommend participating if one comes your way.]]></summary></entry><entry><title type="html">AWS IoT with ESP32: A Lesson in Spite Driven Development</title><link href="https://incpac.github.io/posts/2021-12-05-aws-iot-with-esp32/" rel="alternate" type="text/html" title="AWS IoT with ESP32: A Lesson in Spite Driven Development" /><published>2021-12-05T00:00:00+13:00</published><updated>2021-12-05T00:00:00+13:00</updated><id>https://incpac.github.io/posts/aws-iot-with-esp32</id><content type="html" xml:base="https://incpac.github.io/posts/2021-12-05-aws-iot-with-esp32/"><![CDATA[<style type="text/css">
    figure.highlight { width: 100%; margin-top: 0; }
</style>

<p>Click <a href="#the-business-part">here</a> to get to business.</p>

<p>Earlier this week I was having a chat with an AWS Architect I work with. He was talking about some of the things he’s done 
with a WiFi enabled Arduino. In turn I mentioned some of the sensors I picked up with the intention of getting them connected 
to AWS’s IoT Core service. During this discussion I may have mentioned that I’ve become a bit lazy with my personal projects 
and that it was probably gonna take me a while to actually get anything set up. Now, while his reaction probably didn’t have 
any ill intentions, it miffed me a little. So this weekend I got off my ass, pulled out an ESP32 board, and attempted to get it 
hooked up to IoT Core.</p>

<p>This didn’t go well.</p>

<p>I ended up having a bit more difficulty that I’d like to admit. First up, you need to determine what board you have. I grabbed 
the <a href="https://dl.espressif.com/dl/package_esp32_index.json">ESP32 board package</a> from Esppresif, but then had to figure out what 
the <a href="https://www.jaycar.co.nz/esp32-main-board-with-wifi-and-bluetooth-communication/p/XC3800?pos=2&amp;queryId=2027b53403b7c90344a44eef8ad5c109&amp;sort=relevance">Duinotech ESP32 Main Board</a> 
was based off. It’s the ESP32 Dev Module by the way. Once I confirmed I was able to program the device, I ran a couple of the 
included examples to confirm I was able to connect to my WiFi.</p>

<p>With the device connected to the WiFi, I went looking for a pre-existing example of connecting it to AWS IoT. Naturally, the 
first result in Google was from <a href="https://aws.amazon.com/blogs/compute/building-an-aws-iot-core-device-using-aws-serverless-and-an-esp32/">AWS’s own blog.</a>
Following through the start of this tutorial I was able to manually set up IoT Core and get the ESP32 sending and receiving 
messages. However, as I’ve said previously “manual’s for chumps.” The next move was to get everything set up via Terraform.</p>

<p>Handling the certificate creation was a bit of an issue. I could either do it manually and reference the ARN as a Terraform 
variable, or I could just have Terraform create it. Having Terraform create it would require exporting the private key as 
an output, and ultimately you couldn’t trust the state to be secure. I ended up going this route as it meant I could have 
Terraform create the <code class="language-plaintext highlighter-rouge">secrets.h</code> file. We simply take in the WiFi details as variables and pull the root CA certificate from 
the internet.</p>

<p>Now, while the Terraform config would deploy correctly and I’d end up with an Arduino project that’d compile and burn, the 
ultimate outcome was that nothing was being written or read from IoT Core. After comparing the AWS infrasture to what was built 
manually, and even opening up the policy on the certificate, I still couldn’t get it working. So I copied the <code class="language-plaintext highlighter-rouge">secrets.h</code> from 
the working project into my new one to test, and was able to send and receive messages. This confirmed that the issue was with 
the secrets configuration.</p>

<p>This took way to long to work out. In the end it came down to the URL I was using to connect to IoT Core. Compare 
<code class="language-plaintext highlighter-rouge">xxxxxxxxxxxxx-ats.iot.ap-southeast-2.amazonaws.com</code> and <code class="language-plaintext highlighter-rouge">xxxxxxxxxxxxx.iot.ap-southeast-2.amazonaws.com</code>. I was using the 
<code class="language-plaintext highlighter-rouge">aws_iot_endpoint</code> Terraform data source to retrieve this URL. When you define the data source, you can set an <code class="language-plaintext highlighter-rouge">endpoint_type</code> 
variable to configure the type of URL generated. What I was originally using was <code class="language-plaintext highlighter-rouge">iot:Data</code> when I needed <code class="language-plaintext highlighter-rouge">iot:Data-ATS</code>. I 
originally missed the ats in the URL and the <code class="language-plaintext highlighter-rouge">iot:Data</code> option is the first one the matched my URL without looking too hard.</p>

<p>So ultimately I didn’t end up completing as much as I wanted. It would’ve been nice to have one node sending data and another 
receiving it. However, at least I now have a working base to build off of.</p>

<h2 id="the-business-part">The Business Part</h2>

<p>First thing we want to do is create our Arduino project. The one I ended up working with is from 
<a href="https://github.com/techiesms/AWS-IOT-Core-Home-Automation">TechieSMS.</a> I ended up rearchitecting some of it to help with my 
troubleshooting.</p>

<p>To start off with we want some header stuff</p>

<figure>
    <figcaption>pub_sub_example.ino: Some header stuff</figcaption>
    
<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp">        <span class="cp">#include</span> <span class="cpf">"secrets.h"</span><span class="cp">
</span>        
        <span class="cp">#include</span> <span class="cpf">&lt;ArduinoJson.h&gt;</span><span class="cp">
</span>        <span class="cp">#include</span> <span class="cpf">&lt;PubSubClient.h&gt;</span><span class="cp">
</span>        <span class="cp">#include</span> <span class="cpf">&lt;WiFi.h&gt;</span><span class="cp">
</span>        <span class="cp">#include</span> <span class="cpf">&lt;WiFiClientSecure.h&gt;</span><span class="cp">
</span>        
        <span class="cp">#include</span> <span class="cpf">&lt;Blynk.h&gt;</span><span class="c1"> # included purely to use it's timer function</span><span class="cp">
</span>        
        <span class="cp">#define AWS_IOT_PUB_TOPIC "example/pub"
</span>        <span class="cp">#define AWS_IOT_SUB_TOPIC "example/sub"
</span>        
        <span class="n">BlynkTimer</span> <span class="n">timer</span><span class="p">;</span>
        <span class="n">WiFiClientSecure</span> <span class="n">net</span> <span class="o">=</span> <span class="n">WiFiClientSecure</span><span class="p">();</span>
        <span class="n">PubSubClient</span> <span class="nf">client</span><span class="p">(</span><span class="n">net</span><span class="p">);</span>
    </code></pre></figure>

</figure>

<p>Next we’re gonna add our function to connect to the WiFi:</p>

<figure>
    <figcaption>pub_sub_example.ini: Connect to the WiFi</figcaption>
    
<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp">        <span class="kt">void</span> <span class="nf">connectToWiFi</span><span class="p">()</span> <span class="p">{</span>
          <span class="n">WiFi</span><span class="p">.</span><span class="n">mode</span><span class="p">(</span><span class="n">WIFI_STA</span><span class="p">);</span>
          <span class="n">WiFi</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="n">WIFI_SSID</span><span class="p">,</span> <span class="n">WIFI_PASSWORD</span><span class="p">);</span>
        
          <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"[CORE] Connecting to WiFi ..."</span><span class="p">);</span>
        
          <span class="k">while</span> <span class="p">(</span><span class="n">WiFi</span><span class="p">.</span><span class="n">status</span><span class="p">()</span> <span class="o">!=</span> <span class="n">WL_CONNECTED</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">delay</span><span class="p">(</span><span class="mi">500</span><span class="p">);</span>
            <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"."</span><span class="p">);</span>
          <span class="p">}</span>
        
          <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">" connected"</span><span class="p">);</span>
        <span class="p">}</span>
    </code></pre></figure>

</figure>

<p>We want a function to connect to AWS. We’ll use a certificate to authenticate.</p>

<figure>
    <figcaption>pub_sub_example.ini: Connect to AWS IoT</figcaption>
    
<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp">        <span class="kt">void</span> <span class="nf">connectToIOT</span><span class="p">()</span> <span class="p">{</span>
          <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"[CORE] Connecting to AWS IOT ..."</span><span class="p">);</span>
          
          <span class="c1">// configure wifi client to use the aws iot device credentials</span>
          <span class="n">net</span><span class="p">.</span><span class="n">setCACert</span><span class="p">(</span><span class="n">AWS_CERT_CA</span><span class="p">);</span>
          <span class="n">net</span><span class="p">.</span><span class="n">setCertificate</span><span class="p">(</span><span class="n">AWS_CERT_CRT</span><span class="p">);</span>
          <span class="n">net</span><span class="p">.</span><span class="n">setPrivateKey</span><span class="p">(</span><span class="n">AWS_CERT_PRIVATE</span><span class="p">);</span>
        
          <span class="c1">// connect to the mqtt broker</span>
          <span class="n">client</span><span class="p">.</span><span class="n">setServer</span><span class="p">(</span><span class="n">AWS_IOT_ENDPOINT</span><span class="p">,</span> <span class="mi">8883</span><span class="p">);</span>
          <span class="n">client</span><span class="p">.</span><span class="n">setCallback</span><span class="p">(</span><span class="n">messageHandler</span><span class="p">);</span>
        
          <span class="kt">bool</span> <span class="n">res</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="n">THING_NAME</span><span class="p">);</span>
          <span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="n">res</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">res</span><span class="p">);</span>
            <span class="n">delay</span><span class="p">(</span><span class="mi">500</span><span class="p">);</span>
            <span class="n">res</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">connect</span><span class="p">(</span><span class="n">THING_NAME</span><span class="p">);</span>
          <span class="p">}</span>
        
          <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">client</span><span class="p">.</span><span class="n">connected</span><span class="p">())</span> <span class="p">{</span>
            <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">" connection failed"</span><span class="p">);</span>
            <span class="k">return</span><span class="p">;</span>
          <span class="p">}</span>
        
          <span class="n">client</span><span class="p">.</span><span class="n">subscribe</span><span class="p">(</span><span class="n">AWS_IOT_SUB_TOPIC</span><span class="p">);</span>
          <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">" connected"</span><span class="p">);</span>
        <span class="p">}</span>
    </code></pre></figure>

</figure>

<p>We need a callback function to process messages we receive from AWS.</p>

<figure>
    <figcaption>pub_sub_example.ini: Add a callback to process received messages</figcaption>
    
<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp">        <span class="kt">void</span> <span class="nf">messageHandler</span><span class="p">(</span><span class="kt">char</span><span class="o">*</span> <span class="n">topic</span><span class="p">,</span> <span class="n">byte</span><span class="o">*</span> <span class="n">payload</span><span class="p">,</span> <span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">length</span><span class="p">)</span> <span class="p">{</span>
          <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"[SUB] "</span><span class="p">);</span>
          <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">topic</span><span class="p">);</span>
          <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">": "</span><span class="p">);</span>
        
          <span class="n">StaticJsonDocument</span><span class="o">&lt;</span><span class="mi">200</span><span class="o">&gt;</span> <span class="n">doc</span><span class="p">;</span>
          <span class="n">deserializeJson</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">payload</span><span class="p">);</span> 
          <span class="n">serializeJson</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">Serial</span><span class="p">);</span>
          <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">();</span>
        <span class="p">}</span>
    </code></pre></figure>

</figure>

<p>We also want to be able to send messages.</p>

<figure>
    <figcaption>pub_sub_example.ini: Send messages to AWS</figcaption>
    
<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp">        <span class="kt">void</span> <span class="nf">sendMessage</span><span class="p">()</span> <span class="p">{</span>
          <span class="n">StaticJsonDocument</span><span class="o">&lt;</span><span class="mi">200</span><span class="o">&gt;</span> <span class="n">doc</span><span class="p">;</span>
          <span class="n">doc</span><span class="p">[</span><span class="s">"message"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"Hello World!"</span><span class="p">;</span>
        
          <span class="kt">char</span> <span class="n">jsonBuffer</span><span class="p">[</span><span class="mi">512</span><span class="p">];</span>
          <span class="n">serializeJson</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">jsonBuffer</span><span class="p">);</span>
        
          <span class="n">client</span><span class="p">.</span><span class="n">publish</span><span class="p">(</span><span class="n">AWS_IOT_PUB_TOPIC</span><span class="p">,</span> <span class="n">jsonBuffer</span><span class="p">);</span>
          <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"[PUB] "</span><span class="p">);</span>
          <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">AWS_IOT_PUB_TOPIC</span><span class="p">);</span>
          <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">": "</span><span class="p">);</span>
          <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">jsonBuffer</span><span class="p">);</span>
        <span class="p">}</span>
    </code></pre></figure>

</figure>

<p>And finally the default Arduino functions set up and run the loop.</p>

<figure>
    <figcaption>pub_sub_example.ini: Default arduino stuff</figcaption>
    
<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp">        <span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
          <span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">9600</span><span class="p">);</span>
          
          <span class="n">connectToWiFi</span><span class="p">();</span>
          <span class="n">connectToIOT</span><span class="p">();</span>
        
          <span class="n">timer</span><span class="p">.</span><span class="n">setInterval</span><span class="p">(</span><span class="mi">5000l</span><span class="p">,</span> <span class="n">sendMessage</span><span class="p">);</span>
        <span class="p">}</span>
        
        <span class="kt">void</span> <span class="n">loop</span><span class="p">()</span> <span class="p">{</span>
          <span class="n">timer</span><span class="p">.</span><span class="n">run</span><span class="p">();</span>
          <span class="n">client</span><span class="p">.</span><span class="n">loop</span><span class="p">();</span>
        <span class="p">}</span>
    </code></pre></figure>

</figure>

<p>With that set up we want to create our secrets template.</p>

<figure>
    <figcaption>secrets.h.tpl: Template file for secrets.h</figcaption>
    
<figure class="highlight"><pre><code class="language-cpp" data-lang="cpp">        <span class="cp">#include</span> <span class="cpf">&lt;pgmspace.h&gt;</span><span class="cp">
</span>        
        <span class="cp">#define SECRET
</span>        <span class="cp">#define THING_NAME "${thing_name}"
</span>        
        <span class="k">const</span> <span class="kt">char</span> <span class="n">WIFI_SSID</span><span class="p">[]</span>        <span class="o">=</span> <span class="s">"${wifi_ssid}"</span><span class="p">;</span>
        <span class="k">const</span> <span class="kt">char</span> <span class="n">WIFI_PASSWORD</span><span class="p">[]</span>    <span class="o">=</span> <span class="s">"${wifi_password}"</span><span class="p">;</span>
        <span class="k">const</span> <span class="kt">char</span> <span class="n">AWS_IOT_ENDPOINT</span><span class="p">[]</span> <span class="o">=</span> <span class="s">"${iot_endpoint}"</span><span class="p">;</span>
        
        <span class="c1">// Device Certificate</span>
        <span class="k">static</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">AWS_CERT_CRT</span><span class="p">[]</span> <span class="n">PROGMEM</span> <span class="o">=</span> <span class="s">R"KEY(
        ${device_certificate}
        )KEY"</span><span class="p">;</span>
        
        <span class="c1">// Device Private Key</span>
        <span class="k">static</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">AWS_CERT_PRIVATE</span><span class="p">[]</span> <span class="n">PROGMEM</span> <span class="o">=</span> <span class="s">R"KEY(
        ${private_key}
        )KEY"</span><span class="p">;</span>
        
        <span class="c1">// Amazon Root CA 1</span>
        <span class="k">static</span> <span class="k">const</span> <span class="kt">char</span> <span class="n">AWS_CERT_CA</span><span class="p">[]</span> <span class="n">PROGMEM</span> <span class="o">=</span> <span class="s">R"EOF(
        ${ca_cert}
        )EOF"</span><span class="p">;</span>
    </code></pre></figure>

</figure>

<p>Now on to the Terraform. First we want some variables for our WiFi credentials.</p>

<figure>
    <figcaption>infrastructure.tf: Variables</figcaption>
    
<figure class="highlight"><pre><code class="language-terraform" data-lang="terraform">        <span class="k">variable</span> <span class="s2">"wifi_ssid"</span> <span class="p">{</span>
          <span class="nx">description</span> <span class="p">=</span> <span class="s2">"SSID of the WiFi network to connect the Thing to"</span>
        <span class="p">}</span>
        
        <span class="k">variable</span> <span class="s2">"wifi_password"</span> <span class="p">{</span>
          <span class="nx">description</span> <span class="p">=</span> <span class="s2">"Password for the WiFi to connect the Thing to"</span>
        <span class="p">}</span>
    </code></pre></figure>

</figure>

<p>We need some details about our AWS environment to create ARNs.</p>

<figure>
    <figcaption>infrastructure.tf: AWS environment details</figcaption>
    
<figure class="highlight"><pre><code class="language-terraform" data-lang="terraform">        <span class="k">data</span> <span class="s2">"aws_region"</span> <span class="s2">"current"</span> <span class="p">{}</span>
        <span class="k">data</span> <span class="s2">"aws_caller_identity"</span> <span class="s2">"current"</span> <span class="p">{}</span>

        <span class="k">data</span> <span class="s2">"aws_iot_endpoint"</span> <span class="s2">"endpoint"</span> <span class="p">{</span>
          <span class="nx">endpoint_type</span> <span class="p">=</span> <span class="s2">"iot:Data-ATS"</span>
        <span class="p">}</span>
        
        <span class="k">data</span> <span class="s2">"http"</span> <span class="s2">"iot_root_ca_cert"</span> <span class="p">{</span>
          <span class="nx">url</span> <span class="p">=</span> <span class="s2">"https://www.amazontrust.com/repository/AmazonRootCA1.pem"</span>
        <span class="p">}</span>
    </code></pre></figure>

</figure>

<p>A certificate to authenticate our ESP32 with AWS. We’ll attach a policy to this that allows the Thing to read and write from 
our topics.</p>

<figure>
    <figcaption>infrastructure.tf: IoT authentication</figcaption>
    
<figure class="highlight"><pre><code class="language-terraform" data-lang="terraform">        <span class="k">resource</span> <span class="s2">"aws_iot_certificate"</span> <span class="s2">"pub_sub_example"</span> <span class="p">{</span>
          <span class="nx">active</span> <span class="p">=</span> <span class="kc">true</span>
        <span class="p">}</span>
        
        <span class="k">resource</span> <span class="s2">"aws_iot_policy"</span> <span class="s2">"pub_sub_example"</span> <span class="p">{</span>
          <span class="nx">name</span> <span class="p">=</span> <span class="s2">"pub_sub_example"</span>
          <span class="nx">policy</span> <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_iam_policy_document</span><span class="p">.</span><span class="nx">pub_sub_example</span><span class="p">.</span><span class="nx">json</span>
        <span class="p">}</span>
        
        <span class="k">data</span> <span class="s2">"aws_iam_policy_document"</span> <span class="s2">"pub_sub_example"</span> <span class="p">{</span>
          <span class="nx">statement</span> <span class="p">{</span>
            <span class="nx">actions</span>   <span class="p">=</span> <span class="p">[</span><span class="s2">"iot:Connect"</span><span class="p">]</span>
            <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"arn:aws:iot:</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span><span class="k">}</span><span class="s2">:</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_caller_identity</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">account_id</span><span class="k">}</span><span class="s2">:client/</span><span class="k">${</span><span class="nx">aws_iot_thing</span><span class="p">.</span><span class="nx">pub_sub_example</span><span class="p">.</span><span class="nx">name</span><span class="k">}</span><span class="s2">"</span><span class="p">]</span>
          <span class="p">}</span>
        
          <span class="nx">statement</span> <span class="p">{</span>
            <span class="nx">actions</span>   <span class="p">=</span> <span class="p">[</span><span class="s2">"iot:Subscribe"</span><span class="p">]</span>
            <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"arn:aws:iot:</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span><span class="k">}</span><span class="s2">:</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_caller_identity</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">account_id</span><span class="k">}</span><span class="s2">:topicfilter/example/sub"</span><span class="p">]</span>
          <span class="p">}</span>
        
          <span class="nx">statement</span> <span class="p">{</span>
            <span class="nx">actions</span>   <span class="p">=</span> <span class="p">[</span><span class="s2">"iot:Receive"</span><span class="p">]</span>
            <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"arn:aws:iot:</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span><span class="k">}</span><span class="s2">:</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_caller_identity</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">account_id</span><span class="k">}</span><span class="s2">:topic/example/sub"</span><span class="p">]</span>
          <span class="p">}</span>
        
          <span class="nx">statement</span> <span class="p">{</span>
            <span class="nx">actions</span>   <span class="p">=</span> <span class="p">[</span><span class="s2">"iot:Publish"</span><span class="p">]</span>
            <span class="nx">resources</span> <span class="p">=</span> <span class="p">[</span><span class="s2">"arn:aws:iot:</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_region</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">name</span><span class="k">}</span><span class="s2">:</span><span class="k">${data</span><span class="p">.</span><span class="nx">aws_caller_identity</span><span class="p">.</span><span class="nx">current</span><span class="p">.</span><span class="nx">account_id</span><span class="k">}</span><span class="s2">:topic/example/pub"</span><span class="p">]</span>
          <span class="p">}</span>
        <span class="p">}</span>
        
        <span class="k">resource</span> <span class="s2">"aws_iot_policy_attachment"</span> <span class="s2">"pub_sub_example"</span> <span class="p">{</span>
          <span class="nx">policy</span> <span class="p">=</span> <span class="nx">aws_iot_policy</span><span class="p">.</span><span class="nx">pub_sub_example</span><span class="p">.</span><span class="nx">name</span>
          <span class="nx">target</span> <span class="p">=</span> <span class="nx">aws_iot_certificate</span><span class="p">.</span><span class="nx">pub_sub_example</span><span class="p">.</span><span class="nx">arn</span>
        <span class="p">}</span>
    </code></pre></figure>

</figure>

<p>This certificate gets attached to a Thing.</p>

<figure>
    <figcaption>infrastructure.tf: Create our Thing and attach the certificate</figcaption>
    
<figure class="highlight"><pre><code class="language-terraform" data-lang="terraform">        <span class="k">resource</span> <span class="s2">"aws_iot_thing"</span> <span class="s2">"pub_sub_example"</span> <span class="p">{</span>
          <span class="nx">name</span> <span class="p">=</span> <span class="s2">"pub_sub_example"</span>
        <span class="p">}</span>
        
        <span class="k">resource</span> <span class="s2">"aws_iot_thing_principal_attachment"</span> <span class="s2">"certificate"</span> <span class="p">{</span>
          <span class="nx">principal</span> <span class="p">=</span> <span class="nx">aws_iot_certificate</span><span class="p">.</span><span class="nx">pub_sub_example</span><span class="p">.</span><span class="nx">arn</span>
          <span class="nx">thing</span>     <span class="p">=</span> <span class="nx">aws_iot_thing</span><span class="p">.</span><span class="nx">pub_sub_example</span><span class="p">.</span><span class="nx">name</span>
        <span class="p">}</span>
    </code></pre></figure>

</figure>

<p>And last of all, we want to create our <code class="language-plaintext highlighter-rouge">secrets.h</code> file.</p>

<figure>
    <figcaption>infrastructure.tf: Generate the secrets header for Arduino</figcaption>
    
<figure class="highlight"><pre><code class="language-terraform" data-lang="terraform">        <span class="k">resource</span> <span class="s2">"local_file"</span> <span class="s2">"secrets"</span> <span class="p">{</span>
          <span class="nx">filename</span> <span class="p">=</span> <span class="s2">"</span><span class="k">${</span><span class="nx">path</span><span class="p">.</span><span class="k">module}</span><span class="s2">/secrets.h"</span>
        
          <span class="nx">content</span> <span class="p">=</span> <span class="nx">templatefile</span><span class="p">(</span><span class="s2">"secrets.h.tpl"</span><span class="p">,</span> <span class="p">{</span>
            <span class="nx">thing_name</span>         <span class="p">=</span> <span class="nx">aws_iot_thing</span><span class="p">.</span><span class="nx">pub_sub_example</span><span class="p">.</span><span class="nx">name</span><span class="p">,</span>
            <span class="nx">wifi_ssid</span>          <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">wifi_ssid</span><span class="p">,</span>
            <span class="nx">wifi_password</span>      <span class="p">=</span> <span class="kd">var</span><span class="p">.</span><span class="nx">wifi_password</span><span class="p">,</span>
            <span class="nx">iot_endpoint</span>       <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">aws_iot_endpoint</span><span class="p">.</span><span class="nx">endpoint</span><span class="p">.</span><span class="nx">endpoint_address</span><span class="p">,</span>
            <span class="nx">device_certificate</span> <span class="p">=</span> <span class="nx">aws_iot_certificate</span><span class="p">.</span><span class="nx">pub_sub_example</span><span class="p">.</span><span class="nx">certificate_pem</span><span class="p">,</span>
            <span class="nx">private_key</span>        <span class="p">=</span> <span class="nx">aws_iot_certificate</span><span class="p">.</span><span class="nx">pub_sub_example</span><span class="p">.</span><span class="nx">private_key</span><span class="p">,</span>
            <span class="nx">ca_cert</span>            <span class="p">=</span> <span class="k">data</span><span class="p">.</span><span class="nx">http</span><span class="p">.</span><span class="nx">iot_root_ca_cert</span><span class="p">.</span><span class="nx">body</span>
          <span class="p">})</span>
        <span class="p">}</span>
    </code></pre></figure>

</figure>

<p>With all that in place, our next move is to deploy the Terraform infrastructure. A simple <code class="language-plaintext highlighter-rouge">terraform init</code> and 
<code class="language-plaintext highlighter-rouge">terraform apply</code> should get this done for you. This will create our <code class="language-plaintext highlighter-rouge">secrets.h</code> file used by Arduino.</p>

<p>If you now open the Arduino project in the IDE, you can write it to your device. You’ll need to ensure you have the correct 
board selected and the required libraries installed.</p>

<p>Once you’ve written to the board, open up the serial console and connect to it. If you reset the board you should see the 
following.</p>

<figure>
    
<figure class="highlight"><pre><code class="language-text" data-lang="text">        17:20:51.601 -&gt; [CORE] Connecting to WiFi ........ connected
        17:20:54.559 -&gt; [CORE] Connecting to AWS IOT ... connected
        17:21:01.971 -&gt; [PUB] example/pub: {"message":"Hello World!"}
        17:21:06.956 -&gt; [PUB] example/pub: {"message":"Hello World!"}
    </code></pre></figure>

</figure>

<p>Open up the <a href="https://ap-southeast-2.console.aws.amazon.com/iot/home?region=ap-southeast-2#/test">AWS IoT Test console</a> and 
subscribe to the <code class="language-plaintext highlighter-rouge">#</code> topic. You should start to see the messages coming in.</p>

<p><img width="512" src="/assets/posts/aws-iot-with-esp32/test-console-receive.png" /></p>

<p>Flick over to the publish tab, set the topic as <code class="language-plaintext highlighter-rouge">example/sub</code> and click publish. You should see the message show up in the 
Arduino serial console.</p>

<figure>
    
<figure class="highlight"><pre><code class="language-text" data-lang="text">        17:28:46.976 -&gt; [PUB] example/pub: {"message":"Hello World!"}
        17:28:47.572 -&gt; [SUB] example/sub: {"message":"Hello from AWS IoT console"}
        17:28:51.955 -&gt; [PUB] example/pub: {"message":"Hello World!"}
    </code></pre></figure>

</figure>

<p>And with that we’re done here. This was a bit more work that I had anticipated. I’m not as convinced about the ESP32 as I used 
to be. Maybe an STM32 or something with WiFi, we’ll have to see. I still have that pack of sensors to have a play with.</p>

<p>You can find the source code for this project at <a href="https://github.com/incpac/aws-iot-examples/tree/master/pub_sub_example">GitHub</a></p>]]></content><author><name>Thomas Claridge</name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Playing Around With AWS Rekognition</title><link href="https://incpac.github.io/posts/2021-10-07-playing-with-rekognition/" rel="alternate" type="text/html" title="Playing Around With AWS Rekognition" /><published>2021-10-07T00:00:00+13:00</published><updated>2021-10-07T00:00:00+13:00</updated><id>https://incpac.github.io/posts/playing-with-rekognition</id><content type="html" xml:base="https://incpac.github.io/posts/2021-10-07-playing-with-rekognition/"><![CDATA[<p>A dev I’ve been working with has had an <a href="https://aws.amazon.com/deeplens/">AWS DeepLens</a> tossed on his desk and told to “build 
something awesome with it.” He comes hit me up for inspiration. So I just start Googling what other people have done.</p>

<p>An implementation I came across a few times was to use the DeepLens for general object detection then send the frame off to 
<a href="https://aws.amazon.com/rekognition/">Rekognition</a> for more advanced analysis. Now, I may not have the DeepLens itself in my 
hands, but I can still play around with Rekognitions baked in functions.</p>

<h2 id="ppe-detection">PPE Detection</h2>

<p>A pretty cool function of Rekognition is to detect if a person is wearing gloves or a mask. This is done with the 
<code class="language-plaintext highlighter-rouge">detect_protective_equipment</code> function.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">base64</span>
<span class="kn">import</span> <span class="nn">boto3</span>
<span class="kn">import</span> <span class="nn">json</span>
<span class="kn">import</span> <span class="nn">sys</span>

<span class="n">rekognition</span> <span class="o">=</span> <span class="n">boto3</span><span class="p">.</span><span class="n">client</span><span class="p">(</span><span class="s">'rekognition'</span><span class="p">)</span>

<span class="n">filename</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>

<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
    <span class="n">image</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="n">read</span><span class="p">()</span>

<span class="n">res</span> <span class="o">=</span> <span class="n">rekognition</span><span class="p">.</span><span class="n">detect_protective_equipment</span><span class="p">(</span>
        <span class="n">Image</span><span class="o">=</span><span class="p">{</span>
            <span class="s">'Bytes'</span><span class="p">:</span> <span class="n">image</span> 
        <span class="p">}</span>
    <span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">res</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="bp">True</span><span class="p">))</span>
</code></pre></div></div>

<p>This is the image we’re going to run against:</p>

<p><a title="Javed Anees, CC0, via Wikimedia Commons" href="https://commons.wikimedia.org/wiki/File:Healthcare_workers_wearing_PPE_03.jpg"><img width="512" alt="Healthcare workers wearing PPE 03" src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/8a/Healthcare_workers_wearing_PPE_03.jpg/512px-Healthcare_workers_wearing_PPE_03.jpg" />
</a></p>

<p>Running against the image we get</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"Persons"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"BodyParts"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"Confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">93.98725891113281</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"EquipmentDetections"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                        </span><span class="p">{</span><span class="w">
                            </span><span class="nl">"BoundingBox"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                                </span><span class="nl">"Height"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.1895013451576233</span><span class="p">,</span><span class="w">
                                </span><span class="nl">"Left"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.5032418370246887</span><span class="p">,</span><span class="w">
                                </span><span class="nl">"Top"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.16426366567611694</span><span class="p">,</span><span class="w">
                                </span><span class="nl">"Width"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.12380193173885345</span><span class="w">
                            </span><span class="p">},</span><span class="w">
                            </span><span class="nl">"Confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">96.7356948852539</span><span class="p">,</span><span class="w">
                            </span><span class="nl">"CoversBodyPart"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                                </span><span class="nl">"Confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">99.9734878540039</span><span class="p">,</span><span class="w">
                                </span><span class="nl">"Value"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
                            </span><span class="p">},</span><span class="w">
                            </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"FACE_COVER"</span><span class="w">
                        </span><span class="p">}</span><span class="w">
                    </span><span class="p">],</span><span class="w">
                    </span><span class="nl">"Name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"FACE"</span><span class="w">
                </span><span class="p">},</span><span class="w">
                </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"Confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">97.83950805664062</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"EquipmentDetections"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                        </span><span class="p">{</span><span class="w">
                            </span><span class="nl">"BoundingBox"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                                </span><span class="nl">"Height"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.32249826192855835</span><span class="p">,</span><span class="w">
                                </span><span class="nl">"Left"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.4805949926376343</span><span class="p">,</span><span class="w">
                                </span><span class="nl">"Top"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.0063177612610161304</span><span class="p">,</span><span class="w">
                                </span><span class="nl">"Width"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.20271863043308258</span><span class="w">
                            </span><span class="p">},</span><span class="w">
                            </span><span class="nl">"Confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">97.84740447998047</span><span class="p">,</span><span class="w">
                            </span><span class="nl">"CoversBodyPart"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                                </span><span class="nl">"Confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">90.17861938476562</span><span class="p">,</span><span class="w">
                                </span><span class="nl">"Value"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
                            </span><span class="p">},</span><span class="w">
                            </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HEAD_COVER"</span><span class="w">
                        </span><span class="p">}</span><span class="w">
                    </span><span class="p">],</span><span class="w">
                    </span><span class="nl">"Name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"HEAD"</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">],</span><span class="w">
            </span><span class="nl">"BoundingBox"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"Height"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9791666865348816</span><span class="p">,</span><span class="w">
                </span><span class="nl">"Left"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.41718751192092896</span><span class="p">,</span><span class="w">
                </span><span class="nl">"Top"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.011111111380159855</span><span class="p">,</span><span class="w">
                </span><span class="nl">"Width"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.3921875059604645</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"Confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">99.381103515625</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Id"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w">
        </span><span class="p">}</span><span class="w">
</span><span class="err">...</span><span class="w">
</span></code></pre></div></div>

<h2 id="face-detection">Face Detection</h2>

<p>Moving on from this, we played around with simple face detection.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">base64</span>
<span class="kn">import</span> <span class="nn">boto3</span>
<span class="kn">import</span> <span class="nn">json</span> 
<span class="kn">import</span> <span class="nn">sys</span>

<span class="n">rekognition</span> <span class="o">=</span> <span class="n">boto3</span><span class="p">.</span><span class="n">client</span><span class="p">(</span><span class="s">'rekognition'</span><span class="p">)</span>

<span class="n">filename</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>

<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
    <span class="n">image</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="n">read</span><span class="p">()</span>

<span class="n">res</span> <span class="o">=</span> <span class="n">rekognition</span><span class="p">.</span><span class="n">detect_faces</span><span class="p">(</span>
        <span class="n">Image</span><span class="o">=</span><span class="p">{</span>
            <span class="s">'Bytes'</span><span class="p">:</span> <span class="n">image</span> 
        <span class="p">}</span>
    <span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">res</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">4</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="bp">True</span><span class="p">))</span>
</code></pre></div></div>

<p>However it only detected two faces in the same image.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
    </span><span class="nl">"FaceDetails"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
        </span><span class="p">{</span><span class="w">
            </span><span class="nl">"BoundingBox"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"Height"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.17331033945083618</span><span class="p">,</span><span class="w">
                </span><span class="nl">"Left"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.31406813859939575</span><span class="p">,</span><span class="w">
                </span><span class="nl">"Top"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.24222606420516968</span><span class="p">,</span><span class="w">
                </span><span class="nl">"Width"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.08140096813440323</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"Confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">99.99088287353516</span><span class="p">,</span><span class="w">
            </span><span class="nl">"Landmarks"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
                </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyeLeft"</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"X"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.3451152741909027</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"Y"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.3042142391204834</span><span class="w">
                </span><span class="p">},</span><span class="w">
                </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyeRight"</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"X"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.3797382414340973</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"Y"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.31130728125572205</span><span class="w">
                </span><span class="p">},</span><span class="w">
                </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mouthLeft"</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"X"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.34323662519454956</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"Y"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.38228607177734375</span><span class="w">
                </span><span class="p">},</span><span class="w">
                </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mouthRight"</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"X"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.37218159437179565</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"Y"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.3882775902748108</span><span class="w">
                </span><span class="p">},</span><span class="w">
                </span><span class="p">{</span><span class="w">
                    </span><span class="nl">"Type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"nose"</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"X"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.35931596159935</span><span class="p">,</span><span class="w">
                    </span><span class="nl">"Y"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.35247793793678284</span><span class="w">
                </span><span class="p">}</span><span class="w">
            </span><span class="p">],</span><span class="w">
            </span><span class="nl">"Pose"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"Pitch"</span><span class="p">:</span><span class="w"> </span><span class="mf">-4.998554229736328</span><span class="p">,</span><span class="w">
                </span><span class="nl">"Roll"</span><span class="p">:</span><span class="w"> </span><span class="mf">-0.5071427226066589</span><span class="p">,</span><span class="w">
                </span><span class="nl">"Yaw"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.06535697728395462</span><span class="w">
            </span><span class="p">},</span><span class="w">
            </span><span class="nl">"Quality"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
                </span><span class="nl">"Brightness"</span><span class="p">:</span><span class="w"> </span><span class="mf">76.15126037597656</span><span class="p">,</span><span class="w">
                </span><span class="nl">"Sharpness"</span><span class="p">:</span><span class="w"> </span><span class="mf">53.330047607421875</span><span class="w">
            </span><span class="p">}</span><span class="w">
        </span><span class="p">},</span><span class="w">
</span><span class="err">...</span><span class="w">
</span></code></pre></div></div>

<h2 id="people-detection">People Detection</h2>

<p>An issue with the face detection was that it only detected faces. If you’re looking away or not at the camera at the correct 
angle it’d miss you. However, when toying around I noticed that the PPE detector will report on people not wearing any PPE. 
This means we can use it as a people detector.</p>

<p>It was about this time I was also getting sick of reading JSON outputs, so I tossed OpenCV in the mix.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">base64</span>
<span class="kn">import</span> <span class="nn">boto3</span>
<span class="kn">import</span> <span class="nn">cv2</span>
<span class="kn">import</span> <span class="nn">sys</span>

<span class="n">rekognition</span> <span class="o">=</span> <span class="n">boto3</span><span class="p">.</span><span class="n">client</span><span class="p">(</span><span class="s">'rekognition'</span><span class="p">)</span>

<span class="n">filename</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
<span class="n">output_filename</span> <span class="o">=</span> <span class="n">sys</span><span class="p">.</span><span class="n">argv</span><span class="p">[</span><span class="mi">2</span><span class="p">]</span>

<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
    <span class="n">image</span> <span class="o">=</span> <span class="n">f</span><span class="p">.</span><span class="n">read</span><span class="p">()</span>

<span class="n">res</span> <span class="o">=</span> <span class="n">rekognition</span><span class="p">.</span><span class="n">detect_protective_equipment</span><span class="p">(</span>
        <span class="n">Image</span><span class="o">=</span><span class="p">{</span>
            <span class="s">'Bytes'</span><span class="p">:</span> <span class="n">image</span> 
        <span class="p">}</span>
    <span class="p">)</span>

<span class="n">cv_image</span> <span class="o">=</span> <span class="n">cv2</span><span class="p">.</span><span class="n">imread</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span>
<span class="n">height</span><span class="p">,</span> <span class="n">width</span><span class="p">,</span> <span class="n">_</span> <span class="o">=</span> <span class="n">cv_image</span><span class="p">.</span><span class="n">shape</span>

<span class="k">for</span> <span class="n">person</span> <span class="ow">in</span> <span class="n">res</span><span class="p">[</span><span class="s">'Persons'</span><span class="p">]:</span>
    <span class="n">start_x</span> <span class="o">=</span> <span class="n">width</span> <span class="o">*</span> <span class="n">person</span><span class="p">[</span><span class="s">'BoundingBox'</span><span class="p">][</span><span class="s">'Left'</span><span class="p">]</span>
    <span class="n">start_y</span> <span class="o">=</span> <span class="n">height</span> <span class="o">*</span> <span class="n">person</span><span class="p">[</span><span class="s">'BoundingBox'</span><span class="p">][</span><span class="s">'Top'</span><span class="p">]</span>
    <span class="n">end_x</span> <span class="o">=</span> <span class="n">width</span> <span class="o">*</span> <span class="n">person</span><span class="p">[</span><span class="s">'BoundingBox'</span><span class="p">][</span><span class="s">'Width'</span><span class="p">]</span> <span class="o">+</span> <span class="n">start_x</span>
    <span class="n">end_y</span> <span class="o">=</span> <span class="n">height</span> <span class="o">*</span> <span class="n">person</span><span class="p">[</span><span class="s">'BoundingBox'</span><span class="p">][</span><span class="s">'Height'</span><span class="p">]</span> <span class="o">+</span> <span class="n">start_y</span> 

    <span class="n">start</span> <span class="o">=</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">start_x</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">start_y</span><span class="p">))</span>
    <span class="n">end</span> <span class="o">=</span> <span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="n">end_x</span><span class="p">),</span> <span class="nb">int</span><span class="p">(</span><span class="n">end_y</span><span class="p">))</span>

    <span class="n">cv_image</span> <span class="o">=</span> <span class="n">cv2</span><span class="p">.</span><span class="n">rectangle</span><span class="p">(</span><span class="n">cv_image</span><span class="p">,</span> <span class="n">start</span><span class="p">,</span> <span class="n">end</span><span class="p">,</span> <span class="p">(</span><span class="mi">255</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">),</span> <span class="mi">1</span><span class="p">)</span>

<span class="n">cv2</span><span class="p">.</span><span class="n">imwrite</span><span class="p">(</span><span class="n">output_filename</span><span class="p">,</span> <span class="n">cv_image</span><span class="p">)</span>
</code></pre></div></div>

<p><a title="Javed Anees, CC0, via Wikimedia Commons" href="https://commons.wikimedia.org/wiki/File:Healthcare_workers_wearing_PPE_03.jpg"><img width="512" src="/assets/posts/playing-with-rekognition/healthcare_workers.jpg" /></a></p>

<p><a title="Peter Hershey peterhershey, CC0, via Wikimedia Commons" href="https://commons.wikimedia.org/wiki/File:Man_facing_stairs_and_traffic_(Unsplash).jpg"><img width="512" src="/assets/posts/playing-with-rekognition/man_facing_stairs_and_traffic.jpg" /></a></p>

<p>Now this was only a couple of simple demos, but it showed that it was possible to get up and running with some really cool shit 
super fast. Definitely need to play around with it some more.</p>]]></content><author><name>Thomas Claridge</name></author><summary type="html"><![CDATA[A dev I’ve been working with has had an AWS DeepLens tossed on his desk and told to “build something awesome with it.” He comes hit me up for inspiration. So I just start Googling what other people have done.]]></summary></entry></feed>