<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nathan Glover</title>
    <description>The latest articles on DEV Community by Nathan Glover (@t04glovern).</description>
    <link>https://dev.to/t04glovern</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F91847%2Fcf8f8065-aa21-43fd-9391-f29581844f12.png</url>
      <title>DEV Community: Nathan Glover</title>
      <link>https://dev.to/t04glovern</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/t04glovern"/>
    <language>en</language>
    <item>
      <title>Let's Try - AWS Glue Automatic Compaction for Apache Iceberg</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Sat, 18 Nov 2023 14:15:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/lets-try-aws-glue-automatic-compaction-for-apache-iceberg-1e36</link>
      <guid>https://dev.to/aws-heroes/lets-try-aws-glue-automatic-compaction-for-apache-iceberg-1e36</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Please reach out to me on &lt;a href="https://twitter.com/nathangloverAUS"&gt;Twitter @nathangloverAUS&lt;/a&gt; if you have follow up questions!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This post was originally written on &lt;a href="https://devopstar.com/"&gt;DevOpStar&lt;/a&gt;&lt;/em&gt;. Check it out &lt;a href="https://devopstar.com/2023/11/18/lets-try-aws-glue-automatic-compaction-for-apache-iceberg"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Apache Iceberg has been a table format I've been diving deeper and deeper into as of late. Specifically, I've been using the AWS variant supported by AWS Glue and Amazon Athena. The table format has several powerful features that make it an excellent choice for data lake storage - however, one of those features, compaction, was not supported by AWS Glue until recently.&lt;/p&gt;

&lt;p&gt;Before the release of &lt;a href="https://aws.amazon.com/about-aws/whats-new/2023/11/aws-glue-data-catalog-compaction-iceberg-tables/"&gt;automatic compaction of Apache Iceberg tables in AWS Glue&lt;/a&gt;, you had to run a compaction job to optimize your tables manually. This was a bit of a pain, given it was a manual query you had to run against the table - This query had a timeout as well, and you would receive the dreaded &lt;code&gt;ICEBERG_OPTIMIZE_MORE_RUNS_NEEDED&lt;/code&gt; error after a timeout. This meant you had to run the query again and again and again until it was finally completed.&lt;/p&gt;

&lt;p&gt;In this post, we'll look at how to use the new automatic compaction feature in AWS Glue and how it can help you optimize your Iceberg tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Compaction?
&lt;/h2&gt;

&lt;p&gt;The Apache Iceberg documentation has a great section on &lt;a href="https://iceberg.apache.org/docs/latest/maintenance/"&gt;why compaction is important&lt;/a&gt;. In short, compaction is merging small files into larger files. This is important for several reasons, but the most important are performance and cost. Small files are inefficient to read from and can cause performance issues - especially when reading from S3, where you are charged per request.&lt;/p&gt;

&lt;p&gt;When you run a compaction job, you merge small files into larger ones. This means you are reducing the number of files that need to be read from and the number of requests that need to be made to S3. This can have a significant impact on performance and cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pre-requisites
&lt;/h2&gt;

&lt;p&gt;To follow along with this post, you must do some minor setup with Athena, S3 and Glue. If you have used Athena before, there's a good chance you already have this setup (possibly with a different bucket). Feel free to skip this section if you feel confident you have the required setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  S3
&lt;/h3&gt;

&lt;p&gt;Navigate to the &lt;a href="https://s3.console.aws.amazon.com/s3/home?region=us-west-2"&gt;AWS S3 console&lt;/a&gt; to start with and create an Athena query bucket&lt;/p&gt;

&lt;p&gt;Give the bucket a name such as &lt;code&gt;athena-&amp;lt;region&amp;gt;-&amp;lt;account-id&amp;gt;&lt;/code&gt; and click &lt;strong&gt;Create bucket&lt;/strong&gt;. (For example, &lt;code&gt;athena-us-west-2-123456789012&lt;/code&gt;.)&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: for this post, I'll be using the &lt;strong&gt;us-west-2&lt;/strong&gt; region as it is one of the regions that supports the new automatic compaction feature.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rxCcPsja--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2r14bcgfabfqn2flvrlp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rxCcPsja--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2r14bcgfabfqn2flvrlp.png" alt="Create Athena Query Bucket" width="800" height="296"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Athena
&lt;/h3&gt;

&lt;p&gt;Navigate to the &lt;a href="https://us-west-2.console.aws.amazon.com/athena/home?region=us-west-2#/query-editor/settings"&gt;AWS Athena settings console&lt;/a&gt; and click &lt;strong&gt;Manage&lt;/strong&gt; under the &lt;strong&gt;Query result and encryption settings&lt;/strong&gt; section.&lt;/p&gt;

&lt;p&gt;Change the &lt;strong&gt;Location of query results&lt;/strong&gt; to the bucket you created in the previous step, and click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--I2vHiX3h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/762yr8qmpi200icbl7r3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--I2vHiX3h--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/762yr8qmpi200icbl7r3.png" alt="Update Athena query results location" width="800" height="279"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Glue
&lt;/h3&gt;

&lt;p&gt;Navigate to the &lt;a href="https://us-west-2.console.aws.amazon.com/glue/home?region=us-west-2#/v2/data-catalog/databases"&gt;AWS Glue databases console&lt;/a&gt; and check to see if you have a database called &lt;code&gt;default&lt;/code&gt;. If you do not, create one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--C3WgMqEn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/23pfldyhdj2j1wsf0u5k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--C3WgMqEn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/23pfldyhdj2j1wsf0u5k.png" alt="Create default Glue database" width="800" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Helper Script
&lt;/h3&gt;

&lt;p&gt;To help demonstrate the new automatic compaction feature, I've created a script to do much of the tedious parts of creating an Iceberg table for you. You can find the script &lt;a href="https://gist.github.com/t04glovern/04f6f2934353eb1d0fffd487e9b9b6a3"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;At a high level, the script does the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create an S3 bucket that Iceberg will use to store data and metadata&lt;/li&gt;
&lt;li&gt;Generating 1 million rows of sample data and uploading it to S3&lt;/li&gt;
&lt;li&gt;Create SQL files for:

&lt;ul&gt;
&lt;li&gt;Creating an Iceberg table&lt;/li&gt;
&lt;li&gt;Creating a temporary table for loading sample data&lt;/li&gt;
&lt;li&gt;Loading sample data into Iceberg from the temporary table&lt;/li&gt;
&lt;li&gt;Deleting the tables when we're done&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Creating an IAM role for Glue to use for compaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's grab the script and run it so you can follow along.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: You must set up AWS credentials on your machine for this to work. If you don't have them set up, you can follow the &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html"&gt;AWS CLI configuration guide&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Download the script, make it executable&lt;/span&gt;
curl https://gist.githubusercontent.com/t04glovern/04f6f2934353eb1d0fffd487e9b9b6a3/raw &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; lets-try-iceberg.py &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod&lt;/span&gt; +x lets-try-iceberg.py

&lt;span class="c"&gt;# Create a virtual env (optional)&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; venv .venv
&lt;span class="nb"&gt;source&lt;/span&gt; .venv/bin/activate

&lt;span class="c"&gt;# Install the dependencies&lt;/span&gt;
pip3 &lt;span class="nb"&gt;install &lt;/span&gt;boto3

&lt;span class="c"&gt;# Run the script&lt;/span&gt;
./lets-try-iceberg.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Have a look at the output, and you should see something like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;./lets-try-iceberg.py
&lt;span class="c"&gt;# INFO:root:Bucket iceberg-sample-data-477196 does not exist, creating it...&lt;/span&gt;
&lt;span class="c"&gt;# INFO:root:Uploaded samples.jsonl.gz to s3://iceberg-sample-data-477196/sample-data/samples.jsonl.gz&lt;/span&gt;
&lt;span class="c"&gt;# INFO:root:Created IAM role lets-try-iceberg-compaction-role&lt;/span&gt;
&lt;span class="c"&gt;# INFO:root:Created IAM policy lets-try-iceberg-compaction-policy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you check the directory you ran the script from, you should see several files created:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="c"&gt;# -rw-r--r-- 1 nathan nathan      366 Nov 18 17:26 1-athena-iceberg-create-table.sql&lt;/span&gt;
&lt;span class="c"&gt;# -rw-r--r-- 1 nathan nathan      403 Nov 18 17:26 2-athena-create-temp-table.sql&lt;/span&gt;
&lt;span class="c"&gt;# -rw-r--r-- 1 nathan nathan       94 Nov 18 17:26 3-insert-into-iceberg-from-temp-table.sql&lt;/span&gt;
&lt;span class="c"&gt;# -rw-r--r-- 1 nathan nathan       62 Nov 18 17:26 4-cleanup-temp-table.sql&lt;/span&gt;
&lt;span class="c"&gt;# -rw-r--r-- 1 nathan nathan       50 Nov 18 17:26 5-cleanup-iceberg-table.sql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numbered SQL files are the ones we'll be using to create our Iceberg table and load the sample data into it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create &amp;amp; Load the Iceberg Table
&lt;/h2&gt;

&lt;p&gt;Head over to the &lt;a href="https://us-west-2.console.aws.amazon.com/athena/home?region=us-west-2#/query-editor"&gt;AWS Athena console&lt;/a&gt; and ensure that the &lt;strong&gt;default&lt;/strong&gt; database is selected.&lt;/p&gt;

&lt;p&gt;Take the contents of the &lt;code&gt;1-athena-iceberg-create-table.sql&lt;/code&gt; file and paste it into the query editor. Click &lt;strong&gt;Run&lt;/strong&gt; to create the table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NkDny-0O--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pamj1sw6ycl5vir48drp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NkDny-0O--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pamj1sw6ycl5vir48drp.png" alt="Create Iceberg table" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see a new table called &lt;code&gt;lets_try_iceberg_compaction&lt;/code&gt; under the &lt;strong&gt;Tables&lt;/strong&gt; section of the &lt;strong&gt;default&lt;/strong&gt; database.&lt;/p&gt;

&lt;p&gt;We'll create a temporary table to load the sample data into. Copy the &lt;code&gt;2-athena-create-temp-table.sql&lt;/code&gt; file and paste it into the query editor. Click &lt;strong&gt;Run&lt;/strong&gt; to create the table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--1g7e16nr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dijyd3250u1nd6n8tpbx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--1g7e16nr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dijyd3250u1nd6n8tpbx.png" alt="Create temporary table" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, we'll load the sample data from the temporary table into the Iceberg table. Copy the &lt;code&gt;3-insert-into-iceberg-from-temp-table.sql&lt;/code&gt; file and paste it into the query editor. Click &lt;strong&gt;Run&lt;/strong&gt; to load the data.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: This query will take 15-30 seconds to run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--2pQQwO_T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/92bms37te6g1nbbsg9cb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--2pQQwO_T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/92bms37te6g1nbbsg9cb.png" alt="Load data into Iceberg table" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, let's verify that the data was loaded into the table. Run the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;lets_try_iceberg_compaction&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something like the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_A_R_-6V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o7g0e7vrn9743qmbmojs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_A_R_-6V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/o7g0e7vrn9743qmbmojs.png" alt="Verify data was loaded" width="800" height="486"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Compaction in Action
&lt;/h2&gt;

&lt;p&gt;Now that we have our Iceberg table created and our sample data loaded into it let's enable the new automatic compaction feature and see it in action.&lt;/p&gt;

&lt;p&gt;Navigate to the &lt;a href="https://us-west-2.console.aws.amazon.com/glue/home?region=us-west-2#/v2/data-catalog/tables"&gt;AWS Glue tables console&lt;/a&gt; and select the &lt;strong&gt;lets_try_iceberg_compaction&lt;/strong&gt; table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_NPALMY---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/psyxmn49rbbtcetuajsm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_NPALMY---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/psyxmn49rbbtcetuajsm.png" alt="Select Iceberg table" width="800" height="230"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the bottom of the table details, you should see a tab called &lt;strong&gt;Table optimization&lt;/strong&gt;. Click it, then click &lt;strong&gt;Enable compaction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--buEpKMr_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cvqnwjmi902ifv5pg7ph.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--buEpKMr_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cvqnwjmi902ifv5pg7ph.png" alt="Enable compaction" width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will be prompted to select a role to use for compaction. Select the role created by the script we ran earlier (&lt;code&gt;lets-try-iceberg-compaction-role&lt;/code&gt;), and click &lt;strong&gt;Enable compaction&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--HIYGe3Ci--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5e2sa3olvwo4wzae8m1q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--HIYGe3Ci--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5e2sa3olvwo4wzae8m1q.png" alt="Select compaction role" width="800" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Navigate back to the &lt;a href="https://us-west-2.console.aws.amazon.com/glue/home?region=us-west-2#/v2/data-catalog/tables"&gt;AWS Glue tables console&lt;/a&gt; and select the &lt;strong&gt;lets_try_iceberg_compaction&lt;/strong&gt; table again. Get a coffee, and when you come back, refresh the page. You should see something like the following:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--6TO8gkir--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wqcqzpi512j5i8vg6hro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--6TO8gkir--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wqcqzpi512j5i8vg6hro.png" alt="Compaction in action" width="800" height="216"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At this point, your table should start automatically compacting based on the rate of changes made. According to the blog post &lt;a href="https://aws.amazon.com/blogs/aws/aws-glue-data-catalog-now-supports-automatic-compaction-of-apache-iceberg-tables/"&gt;AWS Glue Data Catalog now supports automatic compaction of Apache Iceberg tables&lt;/a&gt; this rate of change is based on the following:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As Iceberg tables can have multiple partitions, the service calculates this change rate for each partition and schedules managed jobs to compact the partitions where this rate of change breaches a threshold value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You can force another compaction job to verify this by navigating back to the Athena console and rerunning the following query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;lets_try_iceberg_compaction&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;lets_try_iceberg_compaction_sample_data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will insert the sample data into the table again, and you should see another compaction job start in the Glue console.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Mu8Uqyl3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0f9yzyiaiijono5qoxru.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Mu8Uqyl3--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0f9yzyiaiijono5qoxru.png" alt="Force compaction by inserting data" width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;Once you're done, you can use the &lt;code&gt;4-cleanup-temp-table.sql&lt;/code&gt; and &lt;code&gt;5-cleanup-iceberg-table.sql&lt;/code&gt; files to clean up the temporary and Iceberg tables.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 4-cleanup-temp-table.sql&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;lets_try_iceberg_compaction_sample_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 5-cleanup-iceberg-table.sql&lt;/span&gt;
&lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;lets_try_iceberg_compaction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, navigate to the &lt;a href="https://s3.console.aws.amazon.com/s3/home?region=us-west-2"&gt;AWS S3 console&lt;/a&gt; and &lt;strong&gt;Empty&lt;/strong&gt; then &lt;strong&gt;Delete&lt;/strong&gt; the bucket that was created by the script.&lt;/p&gt;

&lt;p&gt;Finally, navigate to the &lt;a href="https://us-east-1.console.aws.amazon.com/iam/home#/roles"&gt;AWS IAM Roles console&lt;/a&gt; and delete the role (&lt;code&gt;lets-try-iceberg-compaction-role&lt;/code&gt;) and policy (&lt;code&gt;lets-try-iceberg-compaction-policy&lt;/code&gt;) that were created by the script.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;This post looked at the new automatic compaction feature for Apache Iceberg tables in AWS Glue. We saw how to enable it and how it works in action. We also saw how to force a compaction job to run and how to clean up the resources we created.&lt;/p&gt;

&lt;p&gt;Something to keep in mind is that this feature's pricing is based on DPU hours. According to the &lt;a href="https://aws.amazon.com/glue/pricing/"&gt;AWS Glue pricing page&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you compact Apache Iceberg tables, and the compaction run for 30 minutes and consume 2 DPUs, you will be billed 2 DPUs * 1/2 hour * $0.44/DPU-Hour, which equals $0.44.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This pricing could add up quickly if you have a high rate of change on your tables - before this feature, you would have to manually run the compaction job, which, although tedious, would incur minimal costs under the Athena pricing model. You will likely want to keep this feature in mind when deciding whether or not to enable this feature. An alternative would be to run the compaction job manually, using a pattern similar to how I did automatic VACUUM jobs in my &lt;a href="https://devopstar.com/2023/07/28/vacuuming-iceberg-with-aws-step-functions/"&gt;Vacuuming Amazon Athena Iceberg with AWS Step Functions&lt;/a&gt; post.&lt;/p&gt;

&lt;p&gt;I'm excited to see this feature released and looking forward to seeing what other features are added to the Glue variant of Iceberg in the future.&lt;/p&gt;

&lt;p&gt;If you have any questions, comments or feedback, please get in touch with me on Twitter &lt;a href="https://twitter.com/nathangloverAUS"&gt;@nathangloverAUS&lt;/a&gt;, &lt;a href="https://www.linkedin.com/in/glovernathan/"&gt;LinkedIn&lt;/a&gt; or leave a comment below!&lt;/p&gt;

</description>
      <category>aws</category>
      <category>athena</category>
      <category>glue</category>
      <category>iceberg</category>
    </item>
    <item>
      <title>Automatic API Key rotation for Amazon Managed Grafana</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Sun, 26 Feb 2023 04:15:00 +0000</pubDate>
      <link>https://dev.to/aws-heroes/automatic-api-key-rotation-for-amazon-managed-grafana-2h68</link>
      <guid>https://dev.to/aws-heroes/automatic-api-key-rotation-for-amazon-managed-grafana-2h68</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Please reach out to me on &lt;a href="https://twitter.com/nathangloverAUS"&gt;Twitter @nathangloverAUS&lt;/a&gt; if you have follow up questions!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This post was originally written on &lt;a href="https://devopstar.com/"&gt;DevOpStar&lt;/a&gt;&lt;/em&gt;. Check it out &lt;a href="https://devopstar.com/2023/02/25/automatic-api-key-rotation-for-amazon-managed-grafana"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Amazon Managed Grafana has an unfortunate limitation where API keys created have a maximum expiration of 30 days. This limitation is quite frustrating if you were trying to automate the deployment of Grafana dashboards and datasources as part of your CI/CD pipeline - as you would need to manually update the API key every 30 days or your deployments would fail.&lt;/p&gt;

&lt;p&gt;This problem is exacerbated by the fact that Amazon Managed Grafana API keys are billed out at the cost of a full user license - so you cannot simply create a new API key every time you deploy your dashboards either. I found this out the expensive way when I didn't read the pricing guide properly and created an API key for each deployment ($8 a key).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hopefully, Amazon will address this limitation in the future&lt;/strong&gt; - but in the meantime, I've written a simple pattern that can be used to automatically rotate an API key every 30 days and store it for use in AWS Secrets Manager. At the end of this post, I outline my plea to Amazon to address this limitation along with some suggestions on how they could do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview of the solution
&lt;/h2&gt;

&lt;p&gt;I've opted to build this example in terraform as it is most likely you will be wanting to deploy Grafana dashboards as part of your CI/CD pipeline. Terraform is arguably the best tool for this - however, there is no reason why the code in this post couldn't be adapted to work with other tools such as CloudFormation or AWS CDK.&lt;/p&gt;

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

&lt;p&gt;The solution is made up of two components:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;AWS Secret is created with a rotation lifecycle policy that will trigger a Lambda function every 30 days&lt;/li&gt;
&lt;li&gt;AWS Lambda Function that will create a new API key in Amazon Managed Grafana and update the AWS Secret with the new key&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;It is expected that you will have already created an Amazon Managed Grafana instance (though you can copy the terraform from this post side by side with your existing terraform and it will work).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The source code for this example can be found at &lt;a href="https://github.com/t04glovern/amazon-managed-grafana-api-key-rotation-terraform"&gt;t04glovern/amazon-managed-grafana-api-key-rotation-terraform&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;We'll begin by looking at the python code that is in charge of rotating the Managed Grafana API keys - as understanding how that works will help when we look at the terraform code.&lt;/p&gt;

&lt;p&gt;Look at &lt;a href="https://github.com/t04glovern/amazon-managed-grafana-api-key-rotation-terraform/blob/main/src/rotate.py"&gt;src/rotate.py&lt;/a&gt; in the source code for this example&lt;/p&gt;

&lt;p&gt;The function is going to expect three environment variables to be present&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;grafana_secret_arn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GRAFANA_API_SECRET_ARN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;grafana_api_key_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GRAFANA_API_KEY_NAME&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;grafana_workspace_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;GRAFANA_WORKSPACE_ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: While you don't technically need to pass the secret ARN as it is available in the Lambda context when the function is invoked by secrets manager, I've opted to do so as it makes it easier to understand.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Next, we attempt to delete any existing API keys with the same name as the one we are about to create. This is to ensure that we clean up old API keys and we don't get billed for duplicates (even though you cannot have multiple API keys with the same name).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;grafana_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_workspace_api_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;keyName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;grafana_api_key_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;workspaceId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;grafana_workspace_id&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;grafana_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ResourceNotFoundException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Following cleanup, we create a new API key with a 30-day expiration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;new_api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;grafana_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_workspace_api_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;keyName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;grafana_api_key_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;keyRole&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ADMIN&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;secondsToLive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2592000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;workspaceId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;grafana_workspace_id&lt;/span&gt;
    &lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;botocore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Error: Failed to generate new API key&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last step is to update the AWS Secret with the new API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;secretmanager_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update_secret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;SecretId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;grafana_secret_arn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;SecretString&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;new_api_key&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;botocore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;message&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Error: Failed to update secret&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we've seen how the Lambda function works, let's look at the terraform code that will deploy it.&lt;/p&gt;

&lt;h3&gt;
  
  
  variables.tf
&lt;/h3&gt;

&lt;p&gt;There are two expected variables for this solution to function - You can however substitute these with hardcoded values or references to your own terraform resources instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"name"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Named identifier for the workspace and related resources"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"grafana_workspace_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;string&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The ID of the Grafana workspace to manage"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  main.tf
&lt;/h3&gt;

&lt;p&gt;The &lt;a href="https://github.com/t04glovern/amazon-managed-grafana-api-key-rotation-terraform/blob/main/main.tf"&gt;main.tf&lt;/a&gt; file is where the bulk of the solution is defined. The first thing we do is create an AWS Secret that will store the API key.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret"&lt;/span&gt; &lt;span class="s2"&gt;"api_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.name}-api-key"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The next part looks complicated but is required to bundle the python code into a zip file that can be deployed to Lambda. The code is zipped up and stored in a &lt;code&gt;zip&lt;/code&gt; directory alongside &lt;code&gt;src&lt;/code&gt; and is not checked into source control.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"random_uuid"&lt;/span&gt; &lt;span class="s2"&gt;"lambda_src_hash"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;keepers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;filename&lt;/span&gt; &lt;span class="nx"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;setunion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;fileset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${path.module}/src/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"*.py"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nx"&gt;fileset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${path.module}/src/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"requirements.txt"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="err"&gt;:&lt;/span&gt;
    &lt;span class="nx"&gt;filename&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;filemd5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${path.module}/src/${filename}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"archive_file"&lt;/span&gt; &lt;span class="s2"&gt;"lambda_zip"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;null_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;install_dependencies&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;type&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"zip"&lt;/span&gt;
  &lt;span class="nx"&gt;source_dir&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/src/"&lt;/span&gt;
  &lt;span class="nx"&gt;excludes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"__pycache__"&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;output_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${path.module}/zip/${random_uuid.lambda_src_hash.result}.zip"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, because AWS Lambda python runtime is not running the most up-to-date boto3 core version - we must force the terraform to install and zip a more recent version of boto3 which complicates this solution quite a lot. If you are reading this post in the future, check out the &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html"&gt;AWS Lambda Python Runtime&lt;/a&gt; page to see if this is still required. As of writing this, version &lt;code&gt;boto3-1.20.32&lt;/code&gt; is the latest version available and &lt;code&gt;boto3-1.26.65&lt;/code&gt; is needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"null_resource"&lt;/span&gt; &lt;span class="s2"&gt;"install_dependencies"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"pip install -r ${path.module}/src/requirements.txt -t ${path.module}/src/  --upgrade"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;triggers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;dependencies_versions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;filemd5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"${path.module}/src/requirements.txt"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skipping over the IAM role and policy terraform (which I won't explain in this post, but you can find in the source code on GitHub), we can see that the Lambda function is created and provided the environment variables we defined earlier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_function"&lt;/span&gt; &lt;span class="s2"&gt;"api_key_rotation"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.name}-api-key-rotation"&lt;/span&gt;

  &lt;span class="nx"&gt;filename&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_zip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_path&lt;/span&gt;
  &lt;span class="nx"&gt;source_code_hash&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;archive_file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;lambda_zip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_base64sha256&lt;/span&gt;

  &lt;span class="nx"&gt;handler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"rotate.lambda_handler"&lt;/span&gt;
  &lt;span class="nx"&gt;runtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"python3.9"&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;variables&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;GRAFANA_API_SECRET_ARN&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
      &lt;span class="nx"&gt;GRAFANA_API_KEY_NAME&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"${var.name}-mangement-api-key"&lt;/span&gt;
      &lt;span class="nx"&gt;GRAFANA_WORKSPACE_ID&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grafana_workspace_id&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;role&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_key_rotation_lambda_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With both the lambda and secret created, a Secret manager rotation schedule is created to invoke the lambda function every 29 days.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_lambda_permission"&lt;/span&gt; &lt;span class="s2"&gt;"secrets_manager_api_key_rotation"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;statement_id&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"AllowExecutionFromSecretsManager"&lt;/span&gt;
  &lt;span class="nx"&gt;action&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"lambda:InvokeFunction"&lt;/span&gt;
  &lt;span class="nx"&gt;function_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_key_rotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;function_name&lt;/span&gt;
  &lt;span class="nx"&gt;principal&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"secretsmanager.amazonaws.com"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret_rotation"&lt;/span&gt; &lt;span class="s2"&gt;"api_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;secret_id&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;rotation_lambda_arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_lambda_function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_key_rotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;rotation_rules&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;automatically_after_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;29&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you only need the terraform to deploy an API key - but do not necessarily use it straight away, then you could get away with not including the final part of the terraform code. However, if there is a requirement to use the API key immediately after deployment in the same Terraform stack, then you will need to add a &lt;code&gt;null_resource&lt;/code&gt; to delay the terraform execution until the secret has been rotated. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I set an arbitrary 20 second delay, but if you wanted to be safe you could increase that.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"null_resource"&lt;/span&gt; &lt;span class="s2"&gt;"api_key_delay"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;provisioner&lt;/span&gt; &lt;span class="s2"&gt;"local-exec"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;command&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sleep 20"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;triggers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;after&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret_rotation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The following terraform can be used to retrieve the API key from the newly created and updated secret.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret"&lt;/span&gt; &lt;span class="s2"&gt;"api_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nx"&gt;null_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_key_delay&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;arn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"aws_secretsmanager_secret_version"&lt;/span&gt; &lt;span class="s2"&gt;"api_key"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;secret_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;aws_secretsmanager_secret&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Plea to Amazon
&lt;/h2&gt;

&lt;p&gt;In this post, we have seen how to use Terraform to deploy an AWS Secret and Lambda function that will rotate the API key for an Amazon Managed Grafana workspace. We did this to get around some frustrating limitations with the way that Amazon Managed Grafana provides API keys that hopefully will be addressed in the future.&lt;/p&gt;

&lt;p&gt;My request to Amazon is for them to make this solution I outlined redundant by doing the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Provide a way to vend short-lived session tokens for API access against Managed Grafanas API for use in CI/CD pipelines.&lt;/li&gt;
&lt;li&gt;If this is not possible, provide a way to create API keys that are not tied to a specific user account - but manage the storage and rotation of these keys in Secrets manager similar to RDS: &lt;a href="https://aws.amazon.com/about-aws/whats-new/2022/12/amazon-rds-integration-aws-secrets-manager/"&gt;https://aws.amazon.com/about-aws/whats-new/2022/12/amazon-rds-integration-aws-secrets-manager/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;If this is not possible, reduce the price of API keys to $0.01 per month so we can use them in CI/CD pipelines without worrying about the cost.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you've had this same problem and can think of a better way to solve it, please let me know on Twitter &lt;a href="https://twitter.com/nathangloverAUS"&gt;@nathangloverAUS&lt;/a&gt; or in the comments below.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>grafana</category>
      <category>cicd</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Sort AWS Config and Security Hub emails with Power Automate</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Sun, 05 Feb 2023 15:39:00 +0000</pubDate>
      <link>https://dev.to/t04glovern/sort-aws-config-and-security-hub-emails-with-power-automate-2bp1</link>
      <guid>https://dev.to/t04glovern/sort-aws-config-and-security-hub-emails-with-power-automate-2bp1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Please reach out to me on &lt;a href="https://twitter.com/nathangloverAUS" rel="noopener noreferrer"&gt;Twitter @nathangloverAUS&lt;/a&gt; if you have follow up questions!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This post was originally written on &lt;a href="https://devopstar.com/" rel="noopener noreferrer"&gt;DevOpStar&lt;/a&gt;&lt;/em&gt;. Check it out &lt;a href="https://devopstar.com/2023/02/05/sort-aws-config-and-security-hub-emails-with-power-automate" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have ever enabled AWS Config or AWS Security Hub before you probably remember getting spammed with compliance emails the day after enabling the functionality. While this is good for audibility it can quickly become a burden to parse through the daily inbox of compliance events.&lt;/p&gt;

&lt;p&gt;This isn't helped by the format of the emails, as they come in as JSON blobs and can be difficult for humans to parse them anyway! Computers, however, are very good at reading JSON so it makes sense that there must be a way to parse these incoming compliance emails and highlight only the important compliance alerting.&lt;/p&gt;

&lt;p&gt;That's where Microsoft Power Automate comes in. This powerful tool can help you automate the process of sorting, categorizing and prioritizing alerts, freeing up your time to focus on more important tasks. In this blog post, we will take a deep dive into how to use Microsoft Power Automate to sort incoming emails from AWS Config and AWS Security Hub.&lt;/p&gt;

&lt;p&gt;Seem below is the final flow that we will be creating!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9qdclizew1l9g4sbfgf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi9qdclizew1l9g4sbfgf.png" alt="Final Power Automate flow for handling config rules" width="730" height="1507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Overview of the problem
&lt;/h2&gt;

&lt;p&gt;The goal is for us to be able to sort these emails into folders based on the &lt;strong&gt;configRuleName&lt;/strong&gt; and then flag any emails that have a &lt;strong&gt;complianceType&lt;/strong&gt; of &lt;strong&gt;NON_COMPLIANT&lt;/strong&gt; as a high priority. This will make it easier for us to find the important alerts and prioritize them for remediation.&lt;/p&gt;

&lt;p&gt;Below is an example of the email that comes in from AWS when a config rule is violated (if you are using AWS Control Tower). Things like the &lt;strong&gt;configRuleName&lt;/strong&gt; and &lt;strong&gt;complianceType&lt;/strong&gt; are the fields that are important to us, as they tell us what rule was violated and whether it was a violation or not.&lt;/p&gt;

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

&lt;p&gt;This example email contains the following JSON block, which we will use later on when constructing the Power Automate flow, so keep it handy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"xxxxxxxx-fc96-3fac-9c63-xxxxxxxxxxxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail-type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Config Rules Compliance Change"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"aws.config"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"account"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"012345678901"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"time"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-01-31T13:56:17Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ap-southeast-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"resourceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sg-xxxxxxxxxxxxxxxxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"awsRegion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ap-southeast-2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"awsAccountId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"012345678901"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"configRuleName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWSControlTower_AWS-GR_RESTRICTED_COMMON_PORTS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"recordVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"configRuleARN"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:config:ap-southeast-2:012345678901:config-rule/config-rule-qlw9ls"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"messageType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ComplianceChangeNotification"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"newEvaluationResult"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"evaluationResultIdentifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"evaluationResultQualifier"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"configRuleName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWSControlTower_AWS-GR_RESTRICTED_COMMON_PORTS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS::EC2::SecurityGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"resourceId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sg-xxxxxxxxxxxxxxxxx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"evaluationMode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DETECTIVE"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"orderingTimestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-01-31T13:56:01.897Z"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"complianceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NON_COMPLIANT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"resultRecordedTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-01-31T13:56:16.785Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"configRuleInvokedTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-01-31T13:56:16.403Z"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"notificationCreationTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2023-01-31T13:56:17.035Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"resourceType"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AWS::EC2::SecurityGroup"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also want to be able to handle emails from AWS Security Hub, which are formatted the same however have a &lt;strong&gt;configRuleName&lt;/strong&gt; that is prefixed with "securityhub" instead of "AWSControlTower".&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the Flow
&lt;/h2&gt;

&lt;p&gt;Start by heading over to &lt;a href="https://make.powerautomate.com/" rel="noopener noreferrer"&gt;https://make.powerautomate.com/&lt;/a&gt; and sign in with your Microsoft account. Once you are logged in, click on "My flows" in the left-hand menu and then click the create button.&lt;/p&gt;

&lt;p&gt;As the emails come into a shared outlook mailbox, we want to create a new flow that triggers when an email arrives in a shared mailbox.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq8x794wvy4hea8i05h0u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fq8x794wvy4hea8i05h0u.png" alt="Create a new Flow that triggers when an email arrives" width="800" height="441"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Rename the rule to something that makes more sense, such as "When a Security Rule Compliance Change email arrives", then modify the advanced settings to specify the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;From&lt;/strong&gt;: &lt;a href="mailto:no-reply@sns.amazonaws.com"&gt;no-reply@sns.amazonaws.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subject Filter&lt;/strong&gt;: Config Rules Compliance Change&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbydxk5mrtuaw19rd91g4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbydxk5mrtuaw19rd91g4.png" alt="Filter incoming emails to match a from address and subject" width="800" height="660"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now that we have the rule to capture the emails we'd like to sort, we need to somehow extract the JSON block that is included in the email - while ignoring all the extra text outside of the JSON block. To do this we must first convert the email HTML to text using the &lt;strong&gt;Html to text&lt;/strong&gt; block.&lt;/p&gt;

&lt;p&gt;In the block settings, select &lt;strong&gt;Body&lt;/strong&gt; as the content to convert.&lt;/p&gt;

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

&lt;p&gt;The next step is probably the most complicated part as we need to do a couple of operations on the text output to extract just the JSON body - and ensure it's in a legal JSON format.&lt;/p&gt;

&lt;p&gt;To do this we will use a &lt;strong&gt;Compose&lt;/strong&gt; block with an expression that will perform the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Split the text output from the email on the character &lt;code&gt;--&lt;/code&gt;, as these characters are directly after the JSON block in the email&lt;/li&gt;
&lt;li&gt;Use the concat function to only operate on the content before the &lt;code&gt;--&lt;/code&gt;, by selecting just index &lt;code&gt;0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Replace any newline encoding strings (&lt;code&gt;\n&lt;/code&gt;) with an empty string, which just strips them out of the data.&lt;/li&gt;
&lt;li&gt;Replace delimited double quotes with just a single quote, removing the delimiter from the string&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;These four steps can be done by pasting the following into the &lt;strong&gt;Expression&lt;/strong&gt; section of the compose block.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;replace(&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;replace(&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;concat(&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;split(&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;outputs('Html_to_text')?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'body'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,'--'&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;),'\n',''&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;),'\&lt;/span&gt;&lt;span class="s2"&gt;"','"&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I would also recommend renaming the block to "Split on &lt;code&gt;--&lt;/code&gt; to retain just JSON from email" to make it clear what the step is doing.&lt;/p&gt;

&lt;p&gt;Now create a &lt;strong&gt;Parse JSON&lt;/strong&gt; block where the Content field is set to the output of the previous block. For the &lt;strong&gt;Schema&lt;/strong&gt; field, select &lt;strong&gt;Generate from sample&lt;/strong&gt; and paste in the JSON block from the email. This will generate a schema that we can use to parse the JSON.&lt;/p&gt;

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

&lt;p&gt;I also recommend renaming the block to "Parse Config Rule JSON" for clarity.&lt;/p&gt;

&lt;p&gt;Now that we have the JSON block parsed, we can begin to set up the logic to sort the emails into folders based on the &lt;strong&gt;configRuleName&lt;/strong&gt;. To do this however we need to set a couple of variables that we can use later on for dealing with some complicated formatting.&lt;/p&gt;

&lt;p&gt;These two variables are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;quote&lt;/strong&gt;: A single quote character that can be referenced as a variable - this is to get around needing to escape single quote characters in expressions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;folderName&lt;/strong&gt;: This variable will store the folder name for the config email to go into. We will use this later on when creating the folder if it doesn't exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create two &lt;strong&gt;Initialize variable&lt;/strong&gt; blocks like so - making sure to also rename the blocks to something more meaningful.&lt;/p&gt;

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

&lt;p&gt;Now we can set the folderName variable based on the &lt;strong&gt;configRuleName&lt;/strong&gt;. To do this we use an if statement to check if the &lt;strong&gt;configRuleName&lt;/strong&gt; starts with "AWSControlTower_AWS-", and if it does, we set the &lt;strong&gt;folderName&lt;/strong&gt; variable to the suffix of the &lt;strong&gt;configRuleName&lt;/strong&gt; after the "AWSControlTower_AWS-" prefix.&lt;/p&gt;

&lt;p&gt;If it is not prefixed with "AWSControlTower_AWS-", we know it is a Security Hub rule and we can set the &lt;strong&gt;folderName&lt;/strong&gt; variable to the &lt;strong&gt;configRuleName&lt;/strong&gt; after the "securityhub-" prefix.&lt;/p&gt;

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

&lt;p&gt;The expression used for the &lt;strong&gt;AWSControlTower&lt;/strong&gt; rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;split(body('Parse_Config_Rule_JSON')?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'configRuleName'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'-')&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;sub(length(split(body('Parse_Config_Rule_JSON')?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'configRuleName'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'-'))&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expression used for the &lt;strong&gt;securityhub&lt;/strong&gt; rule is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;slice(body('Parse_Config_Rule_JSON')?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'configRuleName'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;lastIndexOf(body('Parse_Config_Rule_JSON')?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'detail'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'configRuleName'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'-'))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we need to prepare a variable to store the folder ID of the folder we are going to move the email to. We will also need to set a couple of static variables that are specific to our setup&lt;/p&gt;

&lt;p&gt;Create three new &lt;strong&gt;Initialize variable&lt;/strong&gt; blocks and set the variable names to the following&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;folderId&lt;/strong&gt;: This will store the folder ID of the folder we are going to move the email into.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;mailboxEmail&lt;/strong&gt;: This is the email address of the shared mailbox we are going to move the email into.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;parentFolderId&lt;/strong&gt;: This is the folder ID of the parent folder in which we are going to create the new folders.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To get the &lt;strong&gt;parentFolderId&lt;/strong&gt; - it is easiest to head over to &lt;a href="https://developer.microsoft.com/en-us/graph/graph-explorer" rel="noopener noreferrer"&gt;https://developer.microsoft.com/en-us/graph/graph-explorer&lt;/a&gt; and run the following query to get the folder ID of the parent folder the given folder - replace &lt;code&gt;aws@devopstar.com&lt;/code&gt; with your shared mailbox email address, and &lt;code&gt;Inbox&lt;/code&gt; with the name of the parent folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;https://graph.microsoft.com/v1.0/users/aws@devopstar.com/mailFolders?filter&lt;span class="o"&gt;=&lt;/span&gt;displayName eq &lt;span class="s1"&gt;'Inbox'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you get an error, it is likely that you have not authenticated with the Microsoft Graph API - to do this click your profile in the top right corner and select &lt;strong&gt;Permission consent&lt;/strong&gt;.&lt;/p&gt;

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

&lt;p&gt;Make sure that &lt;strong&gt;Mail.ReadWrite.Shared&lt;/strong&gt; is consented to, and try re-running the query.&lt;/p&gt;

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

&lt;p&gt;From the output of the query, you can see the &lt;strong&gt;id&lt;/strong&gt; of the folder - copy this and paste it into the &lt;strong&gt;parentFolderId&lt;/strong&gt; variable and you should be good to move onto the next step.&lt;/p&gt;

&lt;p&gt;After the variables are set, create a "Send an HTTP request" for Outlook365 block - which allows us to make Microsoft Graph requests within the context of Outlook.&lt;/p&gt;

&lt;p&gt;Set the &lt;strong&gt;Method&lt;/strong&gt; to &lt;strong&gt;GET&lt;/strong&gt; and the &lt;strong&gt;URI&lt;/strong&gt; to the following expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;concat('https://graph.microsoft.com/v&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="err"&gt;/users/',&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;variables('mailboxEmail'),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'/mailFolders/',&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;variables('parentFolderId'),'/childFolders?$filter=displayName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;eq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;',&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;variables('quote'),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;variables('folderName'),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;variables('quote'))&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rename the block to "Check If Folder Exists" for clarity.&lt;/p&gt;

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

&lt;p&gt;To parse the response, we need to create a &lt;strong&gt;Parse JSON&lt;/strong&gt; block and set the &lt;strong&gt;Content&lt;/strong&gt; field to the output &lt;strong&gt;Body&lt;/strong&gt; of the previous block. Change the name of this new block to "Parse Folder Exist Check JSON" and then in the Schema set the following JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@@odata.context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"parentFolderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"childFolderCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"unreadItemCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"totalItemCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"isHidden"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"parentFolderId"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"childFolderCount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"unreadItemCount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"totalItemCount"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"isHidden"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The next step is to check if the folder exists. To do this we need to check the length of the &lt;strong&gt;value&lt;/strong&gt; array in the JSON response. If the length is greater than 0, then we know the folder exists and we can set the &lt;strong&gt;folderId&lt;/strong&gt; variable to the &lt;strong&gt;id&lt;/strong&gt; of the first item in the &lt;strong&gt;value&lt;/strong&gt; array.&lt;/p&gt;

&lt;p&gt;If the length is 0, then we know the folder doesn't exist and we need to create it. I'm going to break this down into two separate steps - but to start with create an if statement block called "Check If Folder exists by checking the length of response value" and set the condition to the following expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;length(body('Parse_Folder_Exist_Check_JSON')?&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;'value'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22li21b0jbg6sufijrbp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F22li21b0jbg6sufijrbp.png" alt="Check if folder exists by checking length of response value" width="800" height="222"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Starting with the &lt;strong&gt;true&lt;/strong&gt; branch, create a new &lt;strong&gt;Set variable&lt;/strong&gt; block and set the &lt;strong&gt;folderId&lt;/strong&gt; variable to the &lt;strong&gt;id&lt;/strong&gt; variable from the "Parse Folder Exist Check JSON" outputs.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwhzr2tlcfxncnvmyksel.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwhzr2tlcfxncnvmyksel.png" alt="Set folderId variable to id from Parse Folder Exist Check JSON" width="800" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It should automatically create an "Apply to each" expression for you when you click on "id" in the dropdown.&lt;/p&gt;

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

&lt;p&gt;Now we need to create the &lt;strong&gt;false&lt;/strong&gt; branch of the if statement. This is where we will create the folder if it doesn't exist. To do this we need to create a new "Send an HTTP request" for Outlook365 block and set the &lt;strong&gt;Method&lt;/strong&gt; to &lt;strong&gt;POST&lt;/strong&gt; and the &lt;strong&gt;URI&lt;/strong&gt; to the following expression:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;concat('https://graph.microsoft.com/v&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="err"&gt;/users/',&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;variables('mailboxEmail'),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;'/mailFolders/',&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;variables('parentFolderId'),'/childFolders')&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the &lt;strong&gt;Body&lt;/strong&gt; to the following expression as well&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;concat('&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"', variables('folderName'), '"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;')&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set the name of this block to "Create Folder" then create a new &lt;strong&gt;Parse JSON&lt;/strong&gt; block and set the &lt;strong&gt;Content&lt;/strong&gt; field to the output &lt;strong&gt;body&lt;/strong&gt; of the previous block. Change the name of this new block to "Parse JSON for Create Folder" and then in the Schema set the following JSON:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"@@odata.context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"displayName"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parentFolderId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"childFolderCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"unreadItemCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"totalItemCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"sizeInBytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"isHidden"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally set the &lt;strong&gt;folderId&lt;/strong&gt; variable to the &lt;strong&gt;id&lt;/strong&gt; variable from the "Parse JSON for Create Folder" output.&lt;/p&gt;

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

&lt;p&gt;We're on the home stretch now. We now create a final if statement block called "Check if New Compliance Result is NOT COMPLIANT" and perform a &lt;em&gt;is not equal to&lt;/em&gt; check on the &lt;strong&gt;complianceResult&lt;/strong&gt; variable and the string "COMPLIANT".&lt;/p&gt;

&lt;p&gt;If the result is not compliant, then we need to flag the email by using the &lt;strong&gt;Flag Email&lt;/strong&gt; block&lt;/p&gt;

&lt;p&gt;Otherwise, we should mark the COMPLIANT emails as read by using the &lt;strong&gt;Mark Email as Read&lt;/strong&gt; block.&lt;/p&gt;

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

&lt;p&gt;The last step is to move the email to the appropriate folder. To do this we use the &lt;strong&gt;Move Email&lt;/strong&gt; block and set the Folder variable to be in the following format where it is prefixed by &lt;code&gt;Id::&lt;/code&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Testing the flow
&lt;/h2&gt;

&lt;p&gt;Now we have the flow built, we can test it. To do this, you will need to forward yourself a copy of the email that AWS SNS sends you - before you do this however, change the "When a Security Rule Compliance Change email arrives" block to trigger on receiving an email from your email address - this is just temporary so you can test the flow.&lt;/p&gt;

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

&lt;p&gt;Once you have forwarded yourself a copy of the email, if the email is NON_COMPLIANT you should see it flagged and moved to a folder named after the &lt;strong&gt;configRuleName&lt;/strong&gt;.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this post, we have built a Power Automate flow that will automatically flag and moves emails from AWS Config and Security hub. This is a great way to keep on top of your AWS Security Hub compliance results and ensure that you are only having to deal with the emails that require your attention.&lt;/p&gt;

&lt;p&gt;If you have any questions or if there's anything in this guide that doesn't make sense, please reach out to me on Twitter &lt;a href="https://twitter.com/nathangloverAUS" rel="noopener noreferrer"&gt;@nathangloverAUS&lt;/a&gt; or via the &lt;a href="https://devopstar.com/#contact" rel="noopener noreferrer"&gt;contact page&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>powerautomate</category>
      <category>securityhub</category>
    </item>
    <item>
      <title>Setup GitHub Codespaces with AWS IAM Roles Anywhere</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Thu, 22 Sep 2022 14:50:35 +0000</pubDate>
      <link>https://dev.to/aws-heroes/setup-github-codespaces-with-aws-iam-roles-anywhere-6p</link>
      <guid>https://dev.to/aws-heroes/setup-github-codespaces-with-aws-iam-roles-anywhere-6p</guid>
      <description>&lt;p&gt;In this blog post, I'm going to try out AWS IAM Roles Anywhere by setting it up for use inside GitHub Codespaces.&lt;/p&gt;

&lt;p&gt;The [offical documentation for IAM &lt;a href="https://docs.aws.amazon.com/rolesanywhere/latest/userguide/getting-started.html" rel="noopener noreferrer"&gt;Roles Anywhere&lt;/a&gt; is okay, however, it is written to only provide two options for users who are getting started:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create your private CA&lt;/strong&gt; - and we're (AWS) not going to show you how to do that.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use ACM PCA&lt;/strong&gt; - a service that costs $400 a month upfront the moment you click the create button.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're a beginner, you'll probably just give up here... This is what I nearly did until I stumbled on &lt;a href="https://github.com/aidansteele" rel="noopener noreferrer"&gt;Aidan Steele&lt;/a&gt;'s &lt;a href="https://github.com/aidansteele/openrolesanywhere" rel="noopener noreferrer"&gt;open-source client&lt;/a&gt; for interacting with IAM Roles Anywhere is a much friendlier way.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I also came across &lt;a href="https://koudingspawn.de/combine-vault-with-iam-anywhere/" rel="noopener noreferrer"&gt;Björn Wenzel's example&lt;/a&gt; of using the offical &lt;code&gt;aws_signing_helper&lt;/code&gt; along with HashiCorp's &lt;code&gt;vault&lt;/code&gt; to manage a certificate authority. This process could also be used to generate the certificates.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Codespaces .devcontainer
&lt;/h2&gt;

&lt;p&gt;By far the easiest way to get started with a custom environment in GitHub Codespaces is to define a &lt;code&gt;.devcontainer/&lt;/code&gt; directory and populate it with a &lt;a href="https://code.visualstudio.com/docs/remote/containers" rel="noopener noreferrer"&gt;Development Container configuration&lt;/a&gt;. I won't go into heaps of detail in this post on the inner workings of the configuration, but at the very least I'll get you started with a configuration that will work for setting up a Codespace environment with IAM Roles Anywhere.&lt;/p&gt;

&lt;p&gt;In the repository that you want to use with GitHub Codespaces, create the following files.&lt;/p&gt;

&lt;h3&gt;
  
  
  .devcontainer/Dockerfile
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# [Choice] Ubuntu version (use ubuntu-22.04 or ubuntu-18.04 on local arm64/Apple Silicon): ubuntu-22.04, ubuntu-20.04, ubuntu-18.04&lt;/span&gt;
ARG &lt;span class="nv"&gt;VARIANT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"jammy"&lt;/span&gt;
FROM mcr.microsoft.com/vscode/devcontainers/base:0-&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VARIANT&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  .devcontainer/devcontainer.json
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;format&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;details,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;see&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;https://aka.ms/devcontainer.json.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;For&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;options,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;see&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;README&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;at:&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;https://github.com/microsoft/vscode-dev-containers/tree/v&lt;/span&gt;&lt;span class="mf"&gt;0.241&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;/containers/ubuntu&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ubuntu"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"dockerfile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dockerfile"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"VARIANT"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ubuntu-22.04"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"postStartCommand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".devcontainer/env.sh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"remoteUser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vscode"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"features"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"git"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"os-provided"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"aws-cli"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"golang"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latest"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sshd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"latest"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  .devcontainer/env.sh
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# Install openrolesanywhere client&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; /tmp/openrolesanywhere &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/openrolesanywhere&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;git clone https://github.com/aidansteele/openrolesanywhere.git /tmp/openrolesanywhere
&lt;span class="nb"&gt;cd&lt;/span&gt; /tmp/openrolesanywhere/cmd/openrolesanywhere
go &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Create Codespace
&lt;/h2&gt;

&lt;p&gt;Commit the changes above to your selected repository then open it up in &lt;a href="https://github.com/codespaces" rel="noopener noreferrer"&gt;GitHub Codespaces&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;Within the example .devcontainer above I've done the hard work of installing the AWS CLI and dependencies needed to run the openrolesanywhere client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Roles Anywhere Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Authenticate with AWS manually (one last time)
&lt;/h3&gt;

&lt;p&gt;I know the whole point of this exercise was to remove the need to get temporary credentials/access tokens, but for the last time (hopefully ever), you'll need to set up your AWS CLI credentials within this Codespace just so we're able to deploy all the resources needed.&lt;/p&gt;

&lt;p&gt;Follow the &lt;a href="https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config" rel="noopener noreferrer"&gt;quick configuration guide&lt;/a&gt; and ensure that whatever user you are deploying this with has at least permissions to interact fully with KMS and IAM.&lt;/p&gt;

&lt;p&gt;For this guide, we'll be deploying to us-east-1 as well, so I've added the following line to my &lt;code&gt;~/.aws/config&lt;/code&gt; file as well just to ensure I'm consistent.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;default]
region &lt;span class="o"&gt;=&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Certificate Authority &amp;amp; KMS Key
&lt;/h3&gt;

&lt;p&gt;Start by deploying a KMS key that will be used as the private key for our certificate authority. An example KMS key can be deployed through the &lt;a href="https://github.com/t04glovern/roles-anywhere-codespaces/blob/main/kms.yml" rel="noopener noreferrer"&gt;kms.yml&lt;/a&gt; template by running the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation deploy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--template-file&lt;/span&gt; ./kms.yml &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--stack-name&lt;/span&gt; openrolesanywhere-kms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output from this stack is the KMS Key ID that will be needed when creating the certificate authority with the &lt;code&gt;openrolesanywhere&lt;/code&gt; CLI. Go ahead now and create the certificate authority and pass it to the KMS Key ID output. This can be done all in one step with the following&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Feel free to tweak other CA options by referencing &lt;a href="https://github.com/aidansteele" rel="noopener noreferrer"&gt;Aidan Steele&lt;/a&gt;'s project &lt;a href="https://github.com/aidansteele/openrolesanywhere" rel="noopener noreferrer"&gt;directly&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;KMS_KEY_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--stack-name&lt;/span&gt; &lt;span class="s2"&gt;"openrolesanywhere-kms"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Stacks[0].Outputs[?OutputKey==`KeyId`].OutputValue'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

openrolesanywhere admin create-ca &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; openrolesanywhere-trust &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--kms-key-id&lt;/span&gt; &lt;span class="nv"&gt;$KMS_KEY_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--validity-duration&lt;/span&gt; 8760h &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--serial-number&lt;/span&gt; 1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--common-name&lt;/span&gt; Codespaces &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--organization&lt;/span&gt; DevOpStar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above creates whats called an IAM Roles Anywhere trust anchor and sets it up to use our certificate authority all in one step.&lt;/p&gt;

&lt;h3&gt;
  
  
  Roles Anywhere Profile
&lt;/h3&gt;

&lt;p&gt;A Roles Anywhere profile now has to be created which maps the trust anchor created prior and a given role that will be assumed by this Codespace eventually.&lt;/p&gt;

&lt;p&gt;You can see an example in the &lt;a href="https://github.com/t04glovern/roles-anywhere-codespaces/blob/main/role.yml" rel="noopener noreferrer"&gt;role.yml&lt;/a&gt; template which provides basic S3 access to a bucket called &lt;code&gt;roles-anywhere-codespaces-example&lt;/code&gt;. I recommend changing this role template to contain a different set of resources based on your requirements before deploying.&lt;/p&gt;

&lt;p&gt;Deploy by running the following commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation deploy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--template-file&lt;/span&gt; ./role.yml &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--stack-name&lt;/span&gt; openrolesanywhere-role &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--capabilities&lt;/span&gt; CAPABILITY_NAMED_IAM

&lt;span class="c"&gt;# arn:aws:iam::012345678912:role/roles-anywhere-codespaces-example&lt;/span&gt;
&lt;span class="nv"&gt;S3_EXAMPLE_ROLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--stack-name&lt;/span&gt; &lt;span class="s2"&gt;"openrolesanywhere-role"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Stacks[0].Outputs[?OutputKey==`RoleArn`].OutputValue'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

openrolesanywhere admin create-profile &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; codespaces-example &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--session-duration&lt;/span&gt; 3600s &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--role-arn&lt;/span&gt; &lt;span class="nv"&gt;$S3_EXAMPLE_ROLE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Backup CA
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;IMPORTANT&lt;/strong&gt;: At this point you have a CA configured within your codespaces that should be backed up and stored somewhere safe/offline. Copy the following files to a safe location for when you want to generate other certificates using the same CA:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~/.config/openrolesanywhere/ca.pem&lt;/li&gt;
&lt;li&gt;~/.config/openrolesanywhere/profile-arn.txt&lt;/li&gt;
&lt;li&gt;~/.config/openrolesanywhere/trust-anchor-arn.txt&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;ca.pem&lt;/code&gt; is a very important file as it cannot be recovered if you lose it. The &lt;code&gt;profile-arn.txt&lt;/code&gt; and &lt;code&gt;trust-anchor-arn.txt&lt;/code&gt; files are just mappings to ARNs of resources that can be retrieved from the AWS console if needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Private Key for signing
&lt;/h3&gt;

&lt;p&gt;Next, we must create a private key that will be used to sign our requests to AWS IAM Roles Anywhere. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Ideally this key should only be used for this Codespace instances and shouldn't be shared across devices (even if it is possible to).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Create a new SSH private key to use for this example&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; rsa &lt;span class="nt"&gt;-b&lt;/span&gt; 4096 &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_rsa_codespaces &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"your_email@example.com"&lt;/span&gt;
&lt;span class="c"&gt;# Press ENTER a bunch&lt;/span&gt;
ssh-add ~/.ssh/id_rsa_codespaces
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;ssh-add -l&lt;/code&gt; to get a list of fingerprints for the keys stored in your SSH agent. In my example, I have this output.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh-add &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="c"&gt;# 4096 SHA256:dxzQKbZvcaQkOpJ55YbZ+1/aWENrgBb8zZIkxl5BRGE your_email@example.com (RSA)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we need to generate a certificate request using the &lt;code&gt;openrolesanywhere&lt;/code&gt; client. This is done by running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openrolesanywhere request-certificate &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--ssh-fingerprint&lt;/span&gt; SHA256:dxzQKbZvcaQkOpJ55YbZ+1/aWENrgBb8zZIkxl5BRGE &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ./key.csr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, the CSR has to be accepted, and a certificate generated by running the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;openrolesanywhere admin accept-request &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--request-file&lt;/span&gt; ./key.csr &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--validity-duration&lt;/span&gt; 8760h &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--serial-number&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--common-name&lt;/span&gt; Codespaces &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--organization&lt;/span&gt; DevOpStar &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; codespaces.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a file called &lt;code&gt;codespaces.pem&lt;/code&gt;, which is our certificate used alongside our private key to make calls to AWS IAM Roles Anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing Certificates
&lt;/h3&gt;

&lt;p&gt;Copy the contents of the &lt;code&gt;codespaces.pem&lt;/code&gt; file to a new file in &lt;code&gt;~/.config/openrolesanywhere/codespaces.pem&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Retrieve the output ARN from our &lt;code&gt;role.yml&lt;/code&gt; stack by running the following command again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws cloudformation describe-stacks &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--stack-name&lt;/span&gt; &lt;span class="s2"&gt;"openrolesanywhere-role"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'Stacks[0].Outputs[?OutputKey==`RoleArn`].OutputValue'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then using this role ARN, update your &lt;code&gt;~/.aws/config&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt;profile default]
credential_process &lt;span class="o"&gt;=&lt;/span&gt; openrolesanywhere credential-process &lt;span class="nt"&gt;--name&lt;/span&gt; codespaces &lt;span class="nt"&gt;--role-arn&lt;/span&gt; arn:aws:iam::012345678912:role/roles-anywhere-codespaces-example
region &lt;span class="o"&gt;=&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test that the Roles Anywhere profile works by running the following command; the output should look similar.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#     "UserId": "AROAUBLxxxxxxxxxxxxx:02",&lt;/span&gt;
&lt;span class="c"&gt;#     "Account": "012345678901",&lt;/span&gt;
&lt;span class="c"&gt;#     "Arn": "arn:aws:sts::012345678901:assumed-role/roles-anywhere-codespaces-example/02"&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configuring Codespaces Secrets
&lt;/h2&gt;

&lt;p&gt;Create the following GitHub repository secrets by navigating to:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;https://github.com/{GithubUser}/{RepoName}/settings/secrets/codespaces&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The following secrets are required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ROLES_ANYWHERE_CERTIFICATE&lt;/strong&gt;: The contents of the &lt;code&gt;codespaces.pem&lt;/code&gt; file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ROLES_ANYWHERE_ROLE&lt;/strong&gt;: The role output from the &lt;code&gt;role.yml&lt;/code&gt; stack&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH_PRIVATE_SIGNING_KEY&lt;/strong&gt;: Private key contents from the &lt;code&gt;~/.ssh/id_rsa_codespaces&lt;/code&gt; file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once those secrets are created, clear out all the ancillary files that were created during this setup process that might be in your repository structure &lt;code&gt;.pem&lt;/code&gt;, &lt;code&gt;.csr&lt;/code&gt;, and possibly the &lt;code&gt;.yml&lt;/code&gt; files (safe to delete).&lt;/p&gt;

&lt;h2&gt;
  
  
  Modify .devcontainer/env.sh
&lt;/h2&gt;

&lt;p&gt;Finally, we can modify the &lt;code&gt;.devcontainer/env.sh&lt;/code&gt; file to now contain the following script that is also available at &lt;a href="https://github.com/t04glovern/roles-anywhere-codespaces/blob/main/.devcontainer/env.sh" rel="noopener noreferrer"&gt;https://github.com/t04glovern/roles-anywhere-codespaces/blob/main/.devcontainer/env.sh&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The script does the following:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Installed openrolesanywhere client&lt;/li&gt;
&lt;li&gt;Imports private key for signing from GitHub secrets into codespaces&lt;/li&gt;
&lt;li&gt;Imports the &lt;code&gt;codespaces.pem&lt;/code&gt; certificate into the &lt;code&gt;openrolesanywhere&lt;/code&gt; client directory&lt;/li&gt;
&lt;li&gt;Configures AWS CLI to use the &lt;code&gt;openrolesanywhere&lt;/code&gt; client for the credentials_process
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt;
&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="c"&gt;# Install openrolesanywhere client&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; /tmp/openrolesanywhere &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/openrolesanywhere&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;fi
&lt;/span&gt;git clone https://github.com/aidansteele/openrolesanywhere.git /tmp/openrolesanywhere
&lt;span class="nb"&gt;cd&lt;/span&gt; /tmp/openrolesanywhere/cmd/openrolesanywhere
go &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;([&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ROLES_ANYWHERE_CERTIFICATE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ROLES_ANYWHERE_ROLE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SSH_PRIVATE_SIGNING_KEY&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;])&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ROLES_ANYWHERE_CERTIFICATE, ROLES_ANYWHERE_ROLE or SSH_PRIVATE_SIGNING_KEY are undefined - skipping AWS auth setup within Codespaces"&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;
  &lt;span class="c"&gt;# Setup SSH Signing key&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.ssh
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; ~/.ssh/id_rsa_codespaces &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/.ssh/id_rsa_codespaces&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;printenv&lt;/span&gt; &lt;span class="s1"&gt;'SSH_PRIVATE_SIGNING_KEY'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.ssh/id_rsa_codespaces
  &lt;span class="nb"&gt;chmod &lt;/span&gt;400 ~/.ssh/id_rsa_codespaces
  ssh-keygen &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; ~/.ssh/id_rsa_codespaces &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.ssh/id_rsa_codespaces.pub

  &lt;span class="c"&gt;# Setup openrolesanywhere config&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/openrolesanywhere
  &lt;span class="nb"&gt;printenv&lt;/span&gt; &lt;span class="s1"&gt;'ROLES_ANYWHERE_CERTIFICATE'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/.config/openrolesanywhere/codespaces.pem

  &lt;span class="c"&gt;# Create credential handler for AWS credential_process&lt;/span&gt;
  &lt;span class="nb"&gt;sudo tee&lt;/span&gt; /opt/roles-anywhere-handler &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;END&lt;/span&gt;&lt;span class="sh"&gt;
#!/bin/bash
eval "&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="sh"&gt;(ssh-agent -s)" &amp;gt; /dev/null
ssh-add ~/.ssh/id_rsa_codespaces &amp;gt; /dev/null
openrolesanywhere credential-process --name codespaces --role-arn &lt;/span&gt;&lt;span class="nv"&gt;$ROLES_ANYWHERE_ROLE&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="no"&gt;END

&lt;/span&gt;  &lt;span class="c"&gt;# Setup AWS config&lt;/span&gt;
  &lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.aws
  &lt;span class="nb"&gt;tee&lt;/span&gt; ~/.aws/config &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;END&lt;/span&gt;&lt;span class="sh"&gt;
[profile default]
credential_process = /opt/roles-anywhere-handler
region = us-east-1
&lt;/span&gt;&lt;span class="no"&gt;END
&lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Commit the changes above to your repository and give it a try by rebuilding your Codespace from scratch - Delete it from &lt;a href="https://github.com/codespaces" rel="noopener noreferrer"&gt;https://github.com/codespaces&lt;/a&gt; and re-create!&lt;/p&gt;

&lt;p&gt;Once booted up you should find that running the following command still returns an authenticated response&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sts get-caller-identity
&lt;span class="c"&gt;# {&lt;/span&gt;
&lt;span class="c"&gt;#     "UserId": "AROAUBLxxxxxxxxxxxxx:02",&lt;/span&gt;
&lt;span class="c"&gt;#     "Account": "012345678901",&lt;/span&gt;
&lt;span class="c"&gt;#     "Arn": "arn:aws:sts::012345678901:assumed-role/roles-anywhere-codespaces-example/02"&lt;/span&gt;
&lt;span class="c"&gt;# }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cleanup
&lt;/h2&gt;

&lt;p&gt;To remove all the resources we've created, you'll need to delete:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Trust anchor and profile from the Roles Anywhere console: &lt;a href="https://us-east-1.console.aws.amazon.com/rolesanywhere/home?region=us-east-1#" rel="noopener noreferrer"&gt;https://us-east-1.console.aws.amazon.com/rolesanywhere/home?region=us-east-1#&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The CloudFormation stacks &lt;code&gt;openrolesanywhere-role&lt;/code&gt; and &lt;code&gt;openrolesanywhere-kms&lt;/code&gt; from &lt;a href="https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1" rel="noopener noreferrer"&gt;https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;GitHub Secrets for your codespace.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;GitHub Codespaces and AWS IAM Roles Anywhere are now configured to work together, but not without some fairly complex configuration. Ultimately I'm hoping to one day see GitHub Codespaces surfacing OIDC/JWT authentication that can be used with how GitHub actions currently work with Identity providers to deploy into accounts.&lt;/p&gt;

&lt;p&gt;For now, this is a fairly Ok solution.&lt;/p&gt;

&lt;p&gt;I'm interested to hear your thoughts on this, and I suppose how it could be improved. Please reach out to me on &lt;a href="https://twitter.com/nathangloverAUS" rel="noopener noreferrer"&gt;Twitter&lt;/a&gt; if you have any further queries or if you have other ways of dealing with this process!&lt;/p&gt;

</description>
      <category>codespaces</category>
      <category>aws</category>
      <category>github</category>
      <category>iam</category>
    </item>
    <item>
      <title>⚡ Supercharge your command line experience</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Sun, 24 Apr 2022 12:48:43 +0000</pubDate>
      <link>https://dev.to/t04glovern/supercharge-your-command-line-experience-53bn</link>
      <guid>https://dev.to/t04glovern/supercharge-your-command-line-experience-53bn</guid>
      <description>&lt;h2&gt;
  
  
  .bashrc and .bash_profile organization
&lt;/h2&gt;

&lt;p&gt;Something that can't be overstated is how handy it can be to split up your dotfile/configuration files that are loaded in when your shell session is kicked off. If you know your way around the shell a little then you've probably seen the &lt;code&gt;.bashrc&lt;/code&gt; and &lt;code&gt;.bash_profile&lt;/code&gt; files.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.bashrc&lt;/code&gt; is run everytime a new non-login shell is opened (a non-login shell would be like when you've logged into a server already, and then opened a new shell). &lt;code&gt;.bash_profile&lt;/code&gt; is executed once on login; so when you ssh into a system or login to a computer.&lt;/p&gt;

&lt;p&gt;Typically you will find the following in your &lt;code&gt;.bashrc&lt;/code&gt; so that all your shell configurtion can live in &lt;code&gt;.bash_profile&lt;/code&gt; and everything is loaded in when a new shell is opened.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$PS1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; ~/.bash_profile&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Doing the above is fine, however I'm a firm believer that it's better to split up the configuration in your &lt;code&gt;.bashrc&lt;/code&gt; file into modular chunks. Adding the following block near the top of your &lt;code&gt;.bash_profile&lt;/code&gt; lets you split up configuration for &lt;code&gt;aliases&lt;/code&gt;, &lt;code&gt;exports&lt;/code&gt; and any other separation you might want to create.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Load the shell dotfiles, and then some:&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;file &lt;span class="k"&gt;in&lt;/span&gt; ~/.&lt;span class="o"&gt;{&lt;/span&gt;exports,aliases&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;unset &lt;/span&gt;file&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above then allows you too create the files &lt;code&gt;.aliases&lt;/code&gt; and &lt;code&gt;.exports&lt;/code&gt; and populate them with any configuration that will be loaded in on shell launch as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  .exports
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;exports&lt;/code&gt; expose envirnoment settings to your system and the software that runs on it. You'd be surprised by the level of customization available for most commonly use tools that can be tweaked by changing specific environment variables.&lt;/p&gt;

&lt;p&gt;Below are a couple fantastic options you might not know about that can be set in your &lt;code&gt;.exports&lt;/code&gt; or &lt;code&gt;.bash_profile&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: the following configuration can live in your &lt;code&gt;.bash_profile&lt;/code&gt; as well if you didn't setup a separate &lt;code&gt;.exports&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Increase Bash history size. Allow 32³ entries; the default is 500.&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HISTSIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'32768'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HISTFILESIZE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;HISTSIZE&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c"&gt;# Omit duplicates and commands that begin with a space from history.&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;HISTCONTROL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ignoreboth'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;# Prefer AU English and use UTF-8. - universal way of setting language&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LANG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'en_AU.UTF-8'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LC_ALL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'en_AU.UTF-8'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  .inputrc
&lt;/h2&gt;

&lt;p&gt;The .inputrc file provides different customization options that affect interactive shell programs (such as Bash and Python). Though any libraries that leverage the &lt;a href="https://tiswww.case.edu/php/chet/readline/rltop.html"&gt;GNU Realine library&lt;/a&gt; can adopt the customiations contained by the .inputrc file.&lt;/p&gt;

&lt;p&gt;Surprisingly, much of this functionality is disabled by default! Below are some of the options I use and a description of what they do.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# When pressing Tab autocomplete, match regardless of case&lt;/span&gt;
&lt;span class="nb"&gt;set &lt;/span&gt;completion-ignore-case on

&lt;span class="c"&gt;# Use the text that has already been typed as the prefix for searching through&lt;/span&gt;
&lt;span class="c"&gt;# commands (i.e. more intelligent Up/Down behavior)&lt;/span&gt;
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\e&lt;/span&gt;&lt;span class="s2"&gt;[B"&lt;/span&gt;: history-search-forward
&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\e&lt;/span&gt;&lt;span class="s2"&gt;[A"&lt;/span&gt;: history-search-backward

&lt;span class="c"&gt;# List all matches in case multiple possible completions are possible&lt;/span&gt;
&lt;span class="nb"&gt;set &lt;/span&gt;show-all-if-ambiguous on
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  .aliases
&lt;/h2&gt;

&lt;p&gt;Aliases are another highly underrated way you can customize your shell experience that allows you to &lt;code&gt;alias&lt;/code&gt; a set of commands to another word/command.&lt;/p&gt;

&lt;p&gt;Below are a couple of great examples of how I use aliases to speed up my shell use experience.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: the following configuration can live in your &lt;code&gt;.bash_profile&lt;/code&gt; as well if you didn't setup a separate &lt;code&gt;.aliases&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Easier navigation: .., ..., ...., ....., ~ and -&lt;/span&gt;
&lt;span class="nb"&gt;alias&lt;/span&gt; ..&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cd .."&lt;/span&gt;
&lt;span class="nb"&gt;alias&lt;/span&gt; ...&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cd ../.."&lt;/span&gt;
&lt;span class="nb"&gt;alias&lt;/span&gt; ....&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cd ../../.."&lt;/span&gt;
&lt;span class="nb"&gt;alias&lt;/span&gt; .....&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cd ../../../.."&lt;/span&gt;
&lt;span class="nb"&gt;alias&lt;/span&gt; ~&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cd ~"&lt;/span&gt; &lt;span class="c"&gt;# `cd` is probably faster to type though&lt;/span&gt;
&lt;span class="nb"&gt;alias&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; -&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"cd -"&lt;/span&gt;

&lt;span class="c"&gt;# Reload the shell (i.e. invoke as a login shell)&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;reload&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"exec &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SHELL&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -l"&lt;/span&gt;

&lt;span class="c"&gt;# Print each PATH entry on a separate line&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'echo -e ${PATH//:/\\n}'&lt;/span&gt;

&lt;span class="c"&gt;# Enable aliases to be sudo’ed&lt;/span&gt;
&lt;span class="nb"&gt;alias sudo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sudo '&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another colourful change you can apply! adding the following config can allow you to customize the color of the output from &lt;code&gt;ls&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Detect which `ls` flavor is in use&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--color&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt; &lt;span class="c"&gt;# GNU `ls`&lt;/span&gt;
    &lt;span class="nv"&gt;colorflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"--color"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LS_COLORS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'no=00:fi=00:di=01;31:ln=01;36:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.zip=01;31:*.z=01;31:*.Z=01;31:*.gz=01;31:*.bz2=01;31:*.deb=01;31:*.rpm=01;31:*.jar=01;31:*.jpg=01;35:*.jpeg=01;35:*.gif=01;35:*.bmp=01;35:*.pbm=01;35:*.pgm=01;35:*.ppm=01;35:*.tga=01;35:*.xbm=01;35:*.xpm=01;35:*.tif=01;35:*.tiff=01;35:*.png=01;35:*.mov=01;35:*.mpg=01;35:*.mpeg=01;35:*.avi=01;35:*.fli=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.ogg=01;35:*.mp3=01;35:*.wav=01;35:'&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="c"&gt;# macOS `ls`&lt;/span&gt;
    &lt;span class="nv"&gt;colorflag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-G"&lt;/span&gt;
    &lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;LSCOLORS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'BxBxhxDxfxhxhxhxhxcxcx'&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="c"&gt;# List all files colorized in long format&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;l&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ls -lF &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;colorflag&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# List all files colorized in long format, excluding . and ..&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;la&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ls -lAF &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;colorflag&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# List only directories&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;lsd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"ls -lF &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;colorflag&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; | grep --color=never '^d'"&lt;/span&gt;

&lt;span class="c"&gt;# Always use color output for `ls`&lt;/span&gt;
&lt;span class="nb"&gt;alias ls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"command ls &lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;colorflag&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Always enable colored `grep` output&lt;/span&gt;
&lt;span class="c"&gt;# Note: `GREP_OPTIONS="--color=auto"` is deprecated, hence the alias usage.&lt;/span&gt;
&lt;span class="nb"&gt;alias grep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'grep --color=auto'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;fgrep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'fgrep --color=auto'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;egrep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'egrep --color=auto'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above config can be seen below, where:&lt;/p&gt;

&lt;p&gt;folders are coloured red, files with execute permissions are green and whereas standard files come up as grey.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bniGQQjF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mc47cttxo0cb4vtjwxb4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bniGQQjF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mc47cttxo0cb4vtjwxb4.png" alt="ls coloured output" width="880" height="256"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Do you have any other awesome command line hacks that work really well for you? Let me know by reaching out to me on twitter and show me &lt;a href="https://twitter.com/nathangloverAUS"&gt;@nathangloverAUS&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>bashprofile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building The Ultimate Cat Watch</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Wed, 08 Jul 2020 14:46:52 +0000</pubDate>
      <link>https://dev.to/t04glovern/building-the-ultimate-cat-watch-43c9</link>
      <guid>https://dev.to/t04glovern/building-the-ultimate-cat-watch-43c9</guid>
      <description>&lt;p&gt;If you've ever met me you'll know that I love my cats. You'll also know that a fair few of my past my tech projects have revolved around my furry family members. So when I got my hands on some brand new &lt;a href="https://www.tindie.com/products/ttgo/lilygor-ttgo-t-watch-2020/"&gt;LilyGo T-Watch 2020&lt;/a&gt;'s recently I knew I had to build the ultimate cat loving accessory for myself.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Watch?
&lt;/h3&gt;

&lt;p&gt;The LilyGo T-Watch 2020 is the recent revision of the LilyGo T-Watch line. These aren't your typical smart watches, as they are completely open source and fully programmable meaning that you have total control over the software that runs on the device.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--idZJHX6f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/vckaswyalqnziaxulyfu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--idZJHX6f--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/vckaswyalqnziaxulyfu.png" alt="LilyGo T-Watch device" width="880" height="188"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not only that but the watch has really awesome spec, especially given the ~$30 price tag. Some of the highlights are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ESP32&lt;/strong&gt; - Regular readers will note how much I love this chip&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1.54 inch LCD&lt;/strong&gt; - Color screen!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FT6236U Touch controller&lt;/strong&gt; - Because a smart watch without a touchscreen isn't really a smart watch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WiFi &amp;amp; Bluetooth&lt;/strong&gt; - Connectivity to keep us... errr... connected&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Triaxial accelerometer&lt;/strong&gt; - Step counter&lt;/li&gt;
&lt;li&gt;Vibration motor, Speaker, Infrared signal sensor and a Real Time Clock (obviously)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All this compressed down into a nice wearable form factor that makes this unit truly awesome as a platform for wearable projects.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Plan
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--o8RrzLJF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/xydhiwm0t9gvzjcv17kc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--o8RrzLJF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/xydhiwm0t9gvzjcv17kc.png" alt="Cat watch plan" width="880" height="271"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I don't start any project without a clear vision of what I want the achieve. In this case it was simple; Two steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take a picture of my cat and put it on the watch (&lt;strong&gt;Critical&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;Display the current time (&lt;strong&gt;Less Critical&lt;/strong&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Programming The Watch
&lt;/h3&gt;

&lt;p&gt;Getting started with programming on the T-Watch is done easiest through the use of &lt;a href="https://platformio.org/"&gt;PlatformIO&lt;/a&gt;. PlatformIO is a truly awesome echo-system for working with embedded devices as it handles all the annoying project configurations and dependency loading for you.&lt;/p&gt;

&lt;h4&gt;
  
  
  Setting up Platform IO
&lt;/h4&gt;

&lt;p&gt;PlatformIO works best with &lt;a href="https://code.visualstudio.com/download"&gt;VSCode&lt;/a&gt;, so if you haven't already got the IDE installed go ahead and do that first. Then once installed, head over to the Extension manager and install &lt;a href="https://marketplace.visualstudio.com/items?itemName=platformio.platformio-ide"&gt;PlatformIO IDE&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3sfL2cai--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/nfpj2b21ah73085237po.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3sfL2cai--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/nfpj2b21ah73085237po.png" alt="PlatformIO IDE extension install (taken from platformio.org)" width="651" height="250"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Importing the Project
&lt;/h4&gt;

&lt;p&gt;Now for you guys following along at home, I'll cut to the chase and give you the code that you can pull down and follow along with. Clone the repo and open it up in VSCode.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/t04glovern/ultimate-cat-watch.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The magic of PlatformIO means that everything else is handled for you! But lets take a look behind the curtains to see how it works. Checking out the &lt;code&gt;platformio.ini&lt;/code&gt; file you can see that everything that is needed to setup your environment for flashing code to the watch is seen there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[env:ttgo-t-watch]&lt;/span&gt;
&lt;span class="py"&gt;platform&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;espressif32&lt;/span&gt;
&lt;span class="py"&gt;board&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;ttgo-t-watch&lt;/span&gt;
&lt;span class="py"&gt;framework&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;arduino&lt;/span&gt;

&lt;span class="py"&gt;monitor_speed&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;115200&lt;/span&gt;

&lt;span class="py"&gt;lib_deps&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
  &lt;span class="err"&gt;TTGO&lt;/span&gt; &lt;span class="err"&gt;TWatch&lt;/span&gt; &lt;span class="err"&gt;Library&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Have a look in the &lt;code&gt;src&lt;/code&gt; folder for all the code related to this project that can be flashed to your watch. Lets dive into what the code does and how it can be changed to display my cat!&lt;/p&gt;

&lt;h4&gt;
  
  
  cat_font.c
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;cat_font.c&lt;/code&gt; is a C (the programming language) formatted font file. Since the watch has no concept of what a font is, we need to convert a font to a format that is basically a list of the pixels that need to be written to the screen to display each letter.&lt;/p&gt;

&lt;p&gt;Luckily there's a fantastic tool that does this for us on the &lt;a href="https://lvgl.io/tools/fontconverter"&gt;LVGL website called fontconverter&lt;/a&gt;. Find a font that you like, I personally picked &lt;a href="https://www.1001freefonts.com/kurri-island.font"&gt;Kurri Island - Måns Grebäck&lt;/a&gt; and downloaded a copy of the &lt;code&gt;.ttf&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;Then navigate back to the &lt;strong&gt;fontconverter&lt;/strong&gt; tool and upload the file. Copy down similar settings to what I've used, and define all the symbols you want converted (I've just used the numbers 0-9).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8gfMJYQO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/d99q1keskeo86iping16.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8gfMJYQO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/d99q1keskeo86iping16.png" alt="LVGL fontconverter settings" width="880" height="647"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: &lt;em&gt;The settings you use might need some tweaking, you'll be able to adjust the settings later once you flash the code and see how it looks&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you hit convert you'll be given an output file called &lt;code&gt;cat_font.c&lt;/code&gt; which can be placed to override the existing file I had in the &lt;a href="https://github.com/t04glovern/ultimate-cat-watch"&gt;ultimate-cat-watch&lt;/a&gt; project.&lt;/p&gt;

&lt;h4&gt;
  
  
  cat_png.c
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;cat_png.c&lt;/code&gt; is similar to the font file from above, however instead we're dealing with an entire image that needs to be converted to a C (the programming language) Array.&lt;/p&gt;

&lt;p&gt;Luckily there's a tool for doing this as well on the &lt;a href="https://lvgl.io/tools/imageconverter"&gt;LVGL website called imageconverter&lt;/a&gt;. Find an image you want to display on the watch; &lt;strong&gt;making sure it has a resolution of roughly 240x240&lt;/strong&gt;. You can use sites like &lt;a href="https://picresize.com/"&gt;picresize.com&lt;/a&gt; to change the dimensions of your image.&lt;/p&gt;

&lt;p&gt;Here's a picture of my cat if instead you just want to use her!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--h7UFJB2y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/llz3jesup7l1783k1s7q.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--h7UFJB2y--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/llz3jesup7l1783k1s7q.jpg" alt="Sample cat image" width="240" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Upload the image and use similar settings to what I have used below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iaeenpVk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/yuzu9f0lh79x52r2rukb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iaeenpVk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/yuzu9f0lh79x52r2rukb.png" alt="LVGL imageconverter settings" width="832" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click convert and you'll be given an output file called &lt;code&gt;cat_png.c&lt;/code&gt; which, like before can be placed to override the existing file I had in the &lt;a href="https://github.com/t04glovern/ultimate-cat-watch"&gt;ultimate-cat-watch&lt;/a&gt; project.&lt;/p&gt;

&lt;h4&gt;
  
  
  main.cpp
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;main.cpp&lt;/code&gt; is the heart of our code and is what brings everything together. Most of the code is copied from the excellent &lt;a href="https://github.com/Xinyuan-LilyGO/TTGO_TWatch_Library/tree/master/examples/LVGL/BatmanDial"&gt;BatmanDial example in the offical TTGO_TWatch_Library&lt;/a&gt; but I've added my own spin.&lt;/p&gt;

&lt;p&gt;We start out in the &lt;code&gt;setup()&lt;/code&gt; function by initializing the watch, clock, and enable the backlight and set it to 150&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;    &lt;span class="n"&gt;Serial&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;115200&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TTGOClass&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;getWatch&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;watch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;begin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;watch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;lvgl_begin&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;rtc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;watch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;rtc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Use compile time&lt;/span&gt;
    &lt;span class="n"&gt;rtc&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;check&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="n"&gt;watch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;openBL&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;//Lower the brightness&lt;/span&gt;
    &lt;span class="n"&gt;watch&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bl&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;adjust&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;150&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then using the LVGL functions we can set the background source image to be the &lt;code&gt;cat_png&lt;/code&gt; C file that was generated&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;    &lt;span class="n"&gt;lv_obj_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;img1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;lv_img_create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lv_scr_act&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;lv_img_set_src&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cat_png&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;lv_obj_align&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LV_ALIGN_CENTER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last important line that may change for you depending on if you changed the font (or the font name) is just below where the image background is set. We create a LVGL style and set the font C file and font color.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;lv_style_t&lt;/span&gt; &lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;lv_style_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;lv_style_set_text_color&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LV_STATE_DEFAULT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LV_COLOR_WHITE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;lv_style_set_text_font&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;LV_STATE_DEFAULT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cat_font&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rest of the code is simply laying out the format of the on-screen clock, and telling the watch how regularly to update the display&lt;/p&gt;

&lt;h3&gt;
  
  
  Flashing the Code
&lt;/h3&gt;

&lt;p&gt;We're finally ready to flash out code! You'll need to plug in the LilyGo T-Watch into your computer using the provided USB cable first obviously.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yKU5Yd-H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/1iwrqbiwmakvkd8at17i.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yKU5Yd-H--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/1iwrqbiwmakvkd8at17i.png" alt="LilyGo T-Watch connect USB cable to computer" width="880" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Head to the bottom of the VSCode screen and click the &lt;code&gt;-&amp;gt;&lt;/code&gt; arrow to begin compiling and uploading the firmware to the connected device&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--F0QpDdpa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/bwoz9pf7v41bg5vedgfi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--F0QpDdpa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/bwoz9pf7v41bg5vedgfi.png" alt="PlatformIO Firmware upload button" width="271" height="46"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the upload is complete, the watch should automatically start up and display the image of my (or your) cat.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EeYdJO2I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/3337xnrnf95fx7mg0f3v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EeYdJO2I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/3337xnrnf95fx7mg0f3v.png" alt="Unimpressed cat with watch" width="656" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;The LilyGo T-Watch 2020 is a fun little hobby device that hits the mark perfectly with the price point. Paired up with PlatformIO and you have a match made in heaven for beginners looking for something fun to do in the embedded space&lt;/p&gt;

&lt;p&gt;I'm interested to hear what you think about the LilyGo T-Watch, hit me up on &lt;a href="https://twitter.com/nathangloverAUS"&gt;Twitter @nathangloverAUS&lt;/a&gt; if you have any thoughts or questions&lt;/p&gt;

</description>
      <category>iot</category>
      <category>cats</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Internet of Things - Recap (June 2020)</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Fri, 03 Jul 2020 15:06:43 +0000</pubDate>
      <link>https://dev.to/t04glovern/internet-of-things-recap-june-2020-21eo</link>
      <guid>https://dev.to/t04glovern/internet-of-things-recap-june-2020-21eo</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Please reach out to me on &lt;a href="https://twitter.com/nathangloverAUS"&gt;Twitter @nathangloverAUS&lt;/a&gt; if you have follow up questions!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This post was originally written on &lt;a href="https://devopstar.com/"&gt;DevOpStar&lt;/a&gt;&lt;/em&gt;. Check it out &lt;a href="https://devopstar.com/newsletter/2020/06/25"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Welcome to the June 2020 issue of &lt;em&gt;the yet to be officially named&lt;/em&gt; &lt;strong&gt;Internet of Things monthly recap&lt;/strong&gt;. Join me as I take you through some of the interesting things I've seen in the world of IoT this month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Community Picks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.meshtastic.org/"&gt;Meshtastic&lt;/a&gt;
&lt;/h3&gt;




&lt;p&gt;&lt;a href="https://www.meshtastic.org/"&gt;Meshtastic&lt;/a&gt; is an open source mesh communication platform that just released its first beta code this month. I've personally fallen for a &lt;a href="https://www.sonnetlabs.com/"&gt;failed Kickstarter&lt;/a&gt; in the past that promised to do exactly what Meshtastic is doing for 5 times the price, so I was extremely excited to see a project that uses easy to find embedded micro controllers (ESP32 in this case)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bAbeKSe2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/wbhgrq5m1b4y89hf3845.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bAbeKSe2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/wbhgrq5m1b4y89hf3845.png" alt="Meshtastic TTGO Board" width="880" height="335"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not only is the &lt;a href="https://github.com/meshtastic/Meshtastic-device"&gt;code open source&lt;/a&gt;, but the project uses &lt;a href="https://platformio.org/"&gt;PlatformIO&lt;/a&gt; to build and test, so you know it won't be a pain to do deployments!&lt;/p&gt;

&lt;p&gt;I've personally put down an order for 2 boards, so once I receive them look forward to some blogs.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://docs.aws.amazon.com/greengrass/latest/developerguide/what-is-gg.html#ggc-version-1.10"&gt;Greengrass Core v1.10.2&lt;/a&gt;
&lt;/h3&gt;




&lt;p&gt;Who doesn't like a new Greengrass Core release? The release cycle for Greengrass has slowed down, so I'll take anything I can get. This month we got &lt;a href="https://docs.aws.amazon.com/greengrass/latest/developerguide/what-is-gg.html#ggc-version-1.10"&gt;Greengrass Core v1.10.2&lt;/a&gt; which aside from some general performance improvement and bug fixes gave us a new parameter for the Greengrass core &lt;code&gt;config.json&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;mqttOperationTimeout&lt;/code&gt; configuration option lets you define the amount of time to allow Greengrass Core to complete a publish, subscribe, or unsubscribe operation with its MQTT connection. By default this setting is 5 seconds.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.youtube.com/watch?v=lcT5jLl538Q"&gt;NVIDIA Jetson Xavier NX Review&lt;/a&gt;
&lt;/h3&gt;




&lt;p&gt;As a proud owner of one of the original NVIDIA Jetson Nano boards I was really excited to hear about the launch of the &lt;a href="https://www.nvidia.com/en-au/autonomous-machines/embedded-systems/jetson-xavier-nx/"&gt;NVIDIA Jetson Xavier NX&lt;/a&gt;. This month we started to see review boards surfacing and &lt;a href="https://www.youtube.com/channel/UC_0CVCfC_3iuHqmyClu59Uw"&gt;ETA PRIME&lt;/a&gt; put up a great review of the board&lt;/p&gt;

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

&lt;p&gt;Some of the standout specs for this board are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hexa-core CPU (NVIDIA custom Carmel ARM-based cores)&lt;/li&gt;
&lt;li&gt;384-core Volta-based GPU&lt;/li&gt;
&lt;li&gt;8GB of LPDDR4x RAM @51.2 GB/s&lt;/li&gt;
&lt;li&gt;HDMI, DisplayPort, 4x USB3.1 ports, WiFi, Bluetooth and Ethernet&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I was going to put in an order for one of these boards from an Australian distributor but the cost is a little bit outside of my budget for now. When I run into a project where the original &lt;a href="https://devopstar.com/bricks/aws-iot-greengrass-jetson-nano"&gt;NVIDIA Jetson Nano&lt;/a&gt; doesn't suffice, I'll definitely make the upgrade though.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://index.ros.org/doc/ros2/Releases/Release-Foxy-Fitzroy/"&gt;ROS 2 Foxy Fitzroy&lt;/a&gt;
&lt;/h3&gt;




&lt;p&gt;Do I have any roboticists out there? Well this month marked the release of &lt;a href="https://index.ros.org/doc/ros2/Releases/Release-Foxy-Fitzroy/"&gt;ROS 2 Foxy Fitzroy&lt;/a&gt;. There was a great post put up on the AWS Blog; &lt;a href="https://aws.amazon.com/blogs/robotics/ros-2-foxy-fitzroy-robot-development/"&gt;ROS 2 Foxy Fitzroy: Setting a new standard for production robot development&lt;/a&gt; that summarized the update beautifully. Some choice cuts include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ROS 2 Foxy topics now automatically publish performance metrics and statistics to help users tune their applications&lt;/li&gt;
&lt;li&gt;Subscription failures due to QoS incompatibility are automatically reported as errors&lt;/li&gt;
&lt;li&gt;Ability to compress and decompress rosbag files while recording and playing data&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://moveit.ros.org/moveit/ros2/2020/02/18/moveit-2-beta-feature-list.html"&gt;MoveIt2&lt;/a&gt; included with ROS 2 Foxy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's been a while since I touched ROS (through RoboMaker) so this update gets me excited to jump back in.&lt;/p&gt;

&lt;h2&gt;
  
  
  DevOpStar Picks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://devopstar.com/bricks/aws-iot-greengrass-jetson-nano"&gt;AWS IoT Greengrass - Jetson Nano&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Have you got a NVIDIA Jetson Nano lying around that you want to put to good use? Why not try out AWS IoT Greengrass on it! This month I wrote a &lt;a href="https://devopstar.com/bricks"&gt;Brick&lt;/a&gt; on getting started with AWS IoT Greengrass specifically for the NVIDIA Jetson Nano. &lt;a href="https://devopstar.com/bricks/aws-iot-greengrass-jetson-nano"&gt;Check out the post to learn more&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;It's targeted specifically at total beginners too, so give it a shot and let me know what you think on &lt;a href="https://twitter.com/nathangloverAUS"&gt;Twitter @nathangloverAUS&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://devopstar.com/bricks/aws-deepracer-training-a-model"&gt;AWS DeepRacer - Training a Model&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Along with Greengrass, I also took a look at what it takes to train a DeepRacer model from scratch, and decided I'd share a simple guide for beginners who are also interested in kick starting their machine learning interests with something very practical.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--RxpHOpeJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/kbuqgzjzqdt5uoj8wq3u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--RxpHOpeJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/kbuqgzjzqdt5uoj8wq3u.png" alt="DeepRacer Results of Guide" width="880" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By the end of the simple tutorial you'll have trained your very own personal model from scratch and customized a fancy car as well. &lt;a href="https://devopstar.com/bricks/aws-deepracer-training-a-model"&gt;Check out the post to learn more&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>iot</category>
      <category>aws</category>
      <category>news</category>
      <category>greengrass</category>
    </item>
    <item>
      <title>AWS DeepRacer - Training a Model</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Sat, 20 Jun 2020 05:36:18 +0000</pubDate>
      <link>https://dev.to/aws-heroes/aws-deepracer-training-a-model-4ep</link>
      <guid>https://dev.to/aws-heroes/aws-deepracer-training-a-model-4ep</guid>
      <description>&lt;p&gt;Welcome to the &lt;strong&gt;AWS DeepRacer - Training a Model&lt;/strong&gt; brick. In this guild we'll go through setting training your very own DeepRacer model from scratch. You don't need to be an expert either!&lt;/p&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;An &lt;a href="https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/"&gt;AWS Account&lt;/a&gt; is all you should need for this tutorial.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: There will be some costs associated with training. These costs are very minimal (less then $2.00) though, and will &lt;strong&gt;likely&lt;/strong&gt; be covered under the Free-tier if you have a new account.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Setting up your Account
&lt;/h3&gt;

&lt;p&gt;The very first thing I recommend doing is to navigate to the &lt;a href="https://console.aws.amazon.com/deepracer/home?region=us-east-1"&gt;AWS DeepRacer console&lt;/a&gt; in the &lt;code&gt;us-east1&lt;/code&gt; region and click on &lt;strong&gt;Get started&lt;/strong&gt; under &lt;strong&gt;Reinforcement learning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--rOmXfs4e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/3q4u3uemnrsff58yj0qw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--rOmXfs4e--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/3q4u3uemnrsff58yj0qw.png" alt="AWS DeepRacer get started" width="880" height="362"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will now work down the list of requirements that will help us train a new model. &lt;strong&gt;Step 0&lt;/strong&gt; is have AWS reset/create some background resources that will be needed to allow AWS DeepRacer to function.&lt;/p&gt;

&lt;p&gt;If this is not the first time working with DeepRacer, you might have ❌'s next to the resources. If this is the case then click &lt;strong&gt;Reset resources&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SvjIceN---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/efs7625zldtf7otuaauk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SvjIceN---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/efs7625zldtf7otuaauk.png" alt="AWS DeepRacer Reset resources" width="812" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the resources have been reset, you will be able to click &lt;strong&gt;Create resources&lt;/strong&gt; next which should eventually turn to ✅'s when completed&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--F-kMFQGn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/7l1n9td2cea3t66hoegh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--F-kMFQGn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/7l1n9td2cea3t66hoegh.png" alt="AWS DeepRacer Create resources" width="809" height="262"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;em&gt;This process can take up to 5 minutes to complete&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create a model
&lt;/h3&gt;

&lt;p&gt;We're up to the fun bit in the tutorial where we can train our very first model. To get started, click &lt;strong&gt;Create model&lt;/strong&gt; under the &lt;strong&gt;Get started&lt;/strong&gt; section.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--qBaD82so--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/ioojel1t12xpxlqq0vpt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--qBaD82so--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/ioojel1t12xpxlqq0vpt.png" alt="AWS DeepRacer Create model" width="809" height="233"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Start by giving your new model a name and description. The name of the model must be unique to your account.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hb3tKVDQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/76a1t1m126d57anz8pcf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hb3tKVDQ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/76a1t1m126d57anz8pcf.png" alt="AWS DeepRacer name and describe model" width="880" height="300"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under environment simulation, select &lt;strong&gt;The 2019 DeepRacer Championship Cup&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;em&gt;If you have a reason to train your model for any of the other tracks, definitely select the one that seems most appropriate for you.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YVz3uHIs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/w7g648sltlbsfj6aoid3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YVz3uHIs--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/w7g648sltlbsfj6aoid3.png" alt="AWS DeepRacer environment simulation" width="880" height="517"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt; at the bottom of the page to move onto &lt;em&gt;Step 2&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In this step we now need to define what kind of race type we would like to train for. Given we're training for just a normal timed race, select &lt;strong&gt;Time trail&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iviQmW5E--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/t3xxjjkbaa7ku3xnhqw8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iviQmW5E--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/t3xxjjkbaa7ku3xnhqw8.png" alt="AWS DeepRacer race type" width="880" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Selecting your &lt;strong&gt;Agent&lt;/strong&gt; (Car type) is next, and this is where the guide can start to become more interesting if you want it to be. By default you can choose to train with &lt;em&gt;The Original DeepRacer&lt;/em&gt; agent which has the following specs&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Isi0T_Wm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/etselfi6h620pne2rv0j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Isi0T_Wm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/etselfi6h620pne2rv0j.png" alt="AWS DeepRacer default vehicle" width="880" height="366"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you are interested in creating a brand new agent (vehicle) from scratch, then move through the next section &lt;strong&gt;Creating an Agent&lt;/strong&gt;. Otherwise, feel free to select &lt;em&gt;The Original DeepRacer&lt;/em&gt; and skip to &lt;strong&gt;Reward function&lt;/strong&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  Creating an Agent [Optional]
&lt;/h4&gt;

&lt;p&gt;We're going to move away from the guide AWS provides us for a second to create our very own &lt;strong&gt;Agent&lt;/strong&gt;. An agent in the context of DeepRacer is the car that drives around the track.&lt;/p&gt;

&lt;p&gt;Navigate to the &lt;a href="https://console.aws.amazon.com/deepracer/home?region=us-east-1#garage"&gt;&lt;strong&gt;Your garage&lt;/strong&gt;&lt;/a&gt; menu under &lt;strong&gt;Reinforcement learning&lt;/strong&gt; and you should be able to see the &lt;em&gt;The Original DeepRacer&lt;/em&gt; agent that is created for you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--auHRX1Hh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/s0pww8tcfaledr4y78om.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--auHRX1Hh--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/s0pww8tcfaledr4y78om.png" alt="AWS DeepRacer default agent" width="880" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This default agent has all the default settings applied, and only uses one camera as its input for information. To understand what I mean by &lt;strong&gt;inputs&lt;/strong&gt;, lets walk through creating a new vehicle by clicking &lt;strong&gt;Build new vehicle&lt;/strong&gt;*.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;em&gt;It is best to understand what kind of track you will be racing on before you create your agent. This is because the track type will heavily influence what types of input data might be considered important when training your model.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You will be presented with a number of options for modifying your agent. Since we picked &lt;strong&gt;The 2019 DeepRacer Championship Cup&lt;/strong&gt; track, lets use it as a foundation for deciding how to build our vehicle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fS2JybF_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4x87ku6hhraj08ti3fyu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fS2JybF_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/4x87ku6hhraj08ti3fyu.png" alt="AWS DeepRacer championship cup track" width="687" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first part of designing our vehicle is to select the modifications. We have options for a variety of different camera setups and even the addition of LIDAR.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Qgipe0sS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/okjg3fkv15xplrnuxjeo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Qgipe0sS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/okjg3fkv15xplrnuxjeo.png" alt="AWS DeepRacer mod specifications" width="872" height="706"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To keep things simple for now though we're going to stick with the &lt;strong&gt;single Camera&lt;/strong&gt; setup.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Having more information from two cameras can be useful, but only if you write a fantastic reward function to use that data in a meaningful way.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For the &lt;strong&gt;Action space&lt;/strong&gt; we're able to select a variety of different steering &amp;amp; speed optimizations. These variables give the vehicle more or less control over how far it is able to turn, and the speed in which it can move.&lt;/p&gt;

&lt;p&gt;Lets breakdown the information we have into some assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Taking a look at the type of track we are racing on, note that there &lt;strong&gt;aren't any hairpin turns&lt;/strong&gt; or anything that could benefit from having a high degree for steering angle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long stretches of track&lt;/strong&gt; that could benefit from a higher then normal top speed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;8 opportunities for turns&lt;/strong&gt; and none of them are particularly wide.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;em&gt;The action list is a set of actions the vehicle can take at any point in time. Sometimes having less available actions can be more beneficial for simpler courses.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here is an example of my desired &lt;strong&gt;Action space&lt;/strong&gt;. Feel free to tinker with these settings, and try something different yourself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--JyX7TJpD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/gmxtbiql0aknmd910pvh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--JyX7TJpD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/gmxtbiql0aknmd910pvh.png" alt="AWS DeepRacer action space" width="880" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, click &lt;strong&gt;Next&lt;/strong&gt; and you'll need to name your vehicle something unique, along with giving it some cool colors&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--UFmk79f---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/lbcdmp2he4favrzk6u8t.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--UFmk79f---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/lbcdmp2he4favrzk6u8t.png" alt="AWS DeepRacer vehicle appearance" width="880" height="697"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click done when you are happy with the car and then head back to the &lt;strong&gt;Create model&lt;/strong&gt; part of the guide. Select your brand new Agent from the list and hit &lt;strong&gt;Next&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--t9Ma4oRS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/e73eaof0hjvdykaz70li.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--t9Ma4oRS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/e73eaof0hjvdykaz70li.png" alt="AWS DeepRacer select agent" width="789" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Reward function
&lt;/h3&gt;

&lt;p&gt;The Reward function is the core of your model; It makes the decisions about what actions to take and when based on a set of (potentially complex) parameters. By default your reward function will look something like the following&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reward_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="s"&gt;'''
    Example of rewarding the agent to follow center line
    '''&lt;/span&gt;

    &lt;span class="c1"&gt;# Read input parameters
&lt;/span&gt;    &lt;span class="n"&gt;track_width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'track_width'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;distance_from_center&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;'distance_from_center'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Calculate 3 markers that are at varying distances away from the center line
&lt;/span&gt;    &lt;span class="n"&gt;marker_1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;track_width&lt;/span&gt;
    &lt;span class="n"&gt;marker_2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;track_width&lt;/span&gt;
    &lt;span class="n"&gt;marker_3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;track_width&lt;/span&gt;

    &lt;span class="c1"&gt;# Give higher reward if the car is closer to center line and vice versa
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;distance_from_center&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;marker_1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;distance_from_center&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;marker_2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;distance_from_center&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;marker_3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;reward&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1e-3&lt;/span&gt;  &lt;span class="c1"&gt;# likely crashed/ close to off track
&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reward&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the simplest way to approach training a model, and for beginners I would recommend just training with it before trying to dive in too deep too fast.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;em&gt;Reward functions &amp;amp; Hyperparameter tweaking is what makes any model GREAT. Once you have a good understanding of how things work, I recommend exploring these advanced settings in more detail. Watch out for future Bricks/Blog posts on these topics!&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When you are ready to begin training, click &lt;strong&gt;Create model&lt;/strong&gt;. The training process will take 60 minutes by default and you can watch how progression is going in the Simulation video stream.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--zIcRcSQV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/wna4u9gvbbk05pw0da7l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--zIcRcSQV--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/wna4u9gvbbk05pw0da7l.png" alt="AWS DeepRacer training process" width="880" height="587"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Model evaluation
&lt;/h3&gt;

&lt;p&gt;Once the training process is finished you will see a ✅ indicating its completion. We are now able to run an evaluation by clicking &lt;strong&gt;Start evaluation&lt;/strong&gt; under the model.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Ejs0XeXd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/el779zoi2p3q3lxars75.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Ejs0XeXd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/el779zoi2p3q3lxars75.png" alt="AWS DeepRacer model evaluation" width="880" height="182"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When prompted for the &lt;strong&gt;Evaluate criteria&lt;/strong&gt;, select &lt;strong&gt;The 2019 DeepRacer Championship Cup&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8T7svSHw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/ltnw122m9oczz3794tqj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8T7svSHw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/ltnw122m9oczz3794tqj.png" alt="AWS DeepRacer evaluate criteria" width="880" height="494"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Under &lt;strong&gt;Race type&lt;/strong&gt; select &lt;strong&gt;Time trial&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--G87D-tPB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/6if6ckfk4o8lwnqwkf29.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--G87D-tPB--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/6if6ckfk4o8lwnqwkf29.png" alt="AWS DeepRacer race type" width="880" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For &lt;strong&gt;Virtual race submission&lt;/strong&gt;, you can opt into competing in the 2020 June Qualifier Time trial. For this example however we will be opting out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9oLkJYah--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/hciidahlr3gznjn6fzig.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9oLkJYah--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/hciidahlr3gznjn6fzig.png" alt="AWS DeepRacer virtual race submission" width="880" height="540"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Start evaluation&lt;/strong&gt; to kick off the ~4 minute job. While it is running, you'll be able to preview how your model is performing in the simulation video stream. This view also includes your lap results.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9yJyET2w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/5bms47lcz78br2gfjqgy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9yJyET2w--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/5bms47lcz78br2gfjqgy.png" alt="AWS DeepRacer training evaluation" width="880" height="568"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;Congratulations on training your very first DeepRacer model! You can now start to build on your model and make adjustments. Once you have a winning model, you can even submit them to free community events and win prizes.&lt;/p&gt;

&lt;p&gt;If you had any issues setting things up, or you have other questions, please let me know by reaching out on Twitter &lt;a href="https://twitter.com/nathangloverAUS"&gt;@nathangloverAUS&lt;/a&gt; or dropping a comment below.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>deepracer</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>AWS IoT Greengrass - Jetson Nano</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Fri, 05 Jun 2020 14:33:55 +0000</pubDate>
      <link>https://dev.to/t04glovern/aws-iot-greengrass-jetson-nano-l9b</link>
      <guid>https://dev.to/t04glovern/aws-iot-greengrass-jetson-nano-l9b</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Please reach out to me on &lt;a href="https://twitter.com/nathangloverAUS" rel="noopener noreferrer"&gt;Twitter @nathangloverAUS&lt;/a&gt; if you have follow up questions!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This post was originally written on &lt;a href="https://devopstar.com/" rel="noopener noreferrer"&gt;DevOpStar&lt;/a&gt;&lt;/em&gt;. Check it out &lt;a href="https://devopstar.com/bricks/aws-iot-greengrass-jetson-nano" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Welcome to the &lt;strong&gt;AWS IoT Greengrass - Jetson Nano&lt;/strong&gt; brick. In this overview we'll go through setting up an NVIDIA Jetson Nano with AWS IoT Greengrass. The specific parts that we will cover can be seen below:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Install NVIDIA Jetson Nano Image&lt;/li&gt;
&lt;li&gt;Create Greengrass Group&lt;/li&gt;
&lt;li&gt;Install AWS IoT Greengrass on Jetson Nano&lt;/li&gt;
&lt;li&gt;Deploy Greengrass Group&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/" rel="noopener noreferrer"&gt;AWS Account&lt;/a&gt; (Free Tier is acceptable)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.balena.io/etcher/" rel="noopener noreferrer"&gt;Etcher&lt;/a&gt; - Used for flashing SD card images&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.nvidia.com/embedded/jetson-nano-developer-kit" rel="noopener noreferrer"&gt;NVIDIA Jetson Nano Developer Kit&lt;/a&gt;, although the second half of this guide will work for any Linux operating system.

&lt;ul&gt;
&lt;li&gt;16GB+ microSD card - Anything smaller and you'll have trouble with the uncompressed Jetson Nano image&lt;/li&gt;
&lt;li&gt;Power, Ethernet, Mouse, Keyboard &amp;amp; HDMI cable for hooking up the Jetson Nano&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h4&gt;
  
  
  Install NVIDIA Jetson Nano Image
&lt;/h4&gt;

&lt;p&gt;Download the Jetson Nano SD card image from the &lt;a href="https://developer.nvidia.com/jetson-nano-sd-card-image" rel="noopener noreferrer"&gt;resources website&lt;/a&gt;. The image is ~6GB compressed so be aware that this can take a while to download.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fpxgk6jii9nf5duey3vwu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fpxgk6jii9nf5duey3vwu.png" alt="Jetson Nano SD card image"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Open up Etcher and complete the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select the &lt;code&gt;.zip&lt;/code&gt; file you just downloaded&lt;/li&gt;
&lt;li&gt;Select the microSD card that you want to flash the image to&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Flash!&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F94iq9rpcz9rasz04t1ic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F94iq9rpcz9rasz04t1ic.png" alt="Jetson Nano flashing image with Etcher"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;em&gt;this flashing process can take quite a while (10-15 minutes)&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once the microSD card has been flashed, remove it from your computer and insert it into the Jetson Nano.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;em&gt;It is worth plugging in an ethernet cable at this stage too if you would like to do a system update on first boot.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Proceed to power on the board and run through the setup process using the baked in wizard. The only screen where you should have to focus your time is when you name the device and pick a username/password&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fu3rit37l0j81hadoce4o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fu3rit37l0j81hadoce4o.png" alt="Jetson Nano system setup user/password"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Create Greengrass Group
&lt;/h4&gt;

&lt;p&gt;In this set of steps we will begin defining out Greengrass group and export a set of certificates that can be used for your Jetson Nano to talk to AWS IoT&lt;/p&gt;

&lt;p&gt;To begin with, navigate to the &lt;a href="https://console.aws.amazon.com/iot/home?region=us-east-1#/greengrassIntro" rel="noopener noreferrer"&gt;AWS IoT portal&lt;/a&gt; and open up the Greengrass option on the left. From here we are going to run through the &lt;strong&gt;Create a Group&lt;/strong&gt; guided setup by clicking the button&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fxsbcyuvxdl0l2w9typ28.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fxsbcyuvxdl0l2w9typ28.png" alt="AWS IoT Greengrass create a group"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Greeted with two boxes, you are asked which type of setup you would like to run through. For the purpose of this getting started guide, we will be selecting &lt;strong&gt;Use default creation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fclg31y2xjw1roisjoixu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fclg31y2xjw1roisjoixu.png" alt="AWS IoT Greengrass default group creation"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Time to name the Greengrass group, this name needs to be unique within the region of your account. In this guide we've gone with &lt;code&gt;demo-group&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fr88kfxwyvta1g2ceoyib.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fr88kfxwyvta1g2ceoyib.png" alt="AWS IoT Greengrass demo group naming"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Core function is used by your Greengrass device to communicate with AWS IoT and other IoT devices on your local network. For this guide we will go with the name that is provided to us by default &lt;code&gt;demo-group_Core&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F93dj6jfh0kf70u0qb470.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F93dj6jfh0kf70u0qb470.png" alt="AWS IoT Greengrass name Greengrass group core"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, you will be prompted to understand that the creation task with perform a number of actions for you under the hood. Click &lt;strong&gt;Create Group and Core&lt;/strong&gt; to have all the actions taken.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: &lt;em&gt;If you are interested in learning how you can deploy this resources with AWS CloudFormation, check out &lt;a href="https://github.com/t04glovern/aws-greengrass-cfn/blob/master/.blog/README.md" rel="noopener noreferrer"&gt;t04glovern/aws-greengrass-cfn&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx9u9hb1cm3z1j85yrmvv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fx9u9hb1cm3z1j85yrmvv.png" alt="AWS IoT Greengrass create group and core"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Time to celebrate as you have created your Greengrass group and core! Simply download the &lt;code&gt;tar.gz&lt;/code&gt; files and keep them somewhere safe for the next steps.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fjx869kcuv64eh9wvmg1k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fjx869kcuv64eh9wvmg1k.png" alt="AWS IoT Greengrass download resources"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Finish&lt;/strong&gt; to complete this section. In the next section we will take our Greengrass resources and install them on our Jetson Nano.&lt;/p&gt;

&lt;h4&gt;
  
  
  Install AWS IoT Greengrass on Jetson Nano
&lt;/h4&gt;

&lt;p&gt;Jumping over to your Jetson Nano device, you will need some way to work on the system. You can either:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH to the Jetson Nano: &lt;code&gt;ssh &amp;lt;username&amp;gt;@&amp;lt;ipaddress&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Plug in keyboard/mouse and work directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first thing you will need to do is copy the &lt;code&gt;tar.gz&lt;/code&gt; files from the previous steps over. This can either be accomplished &lt;strong&gt;by a USB&lt;/strong&gt;, or &lt;strong&gt;SCP&lt;/strong&gt; (if you feel confident with the command line)&lt;/p&gt;

&lt;p&gt;An example of using SCP to copy the file across:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp xxxxxxxxx-setup.tar.gz &amp;lt;username&amp;gt;@&amp;lt;ipaddress&amp;gt;:/home/&amp;lt;username&amp;gt;

&lt;span class="c"&gt;# Example of my command&lt;/span&gt;
scp 551aca43d9-setup.tar.gz devopstar@devopstar.local:/home/devopstar
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the files onboard, its time to install Greengrass on the Jetson Nano. To do this run the following commands in order.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# create user ggc_user and group ggc_group&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;adduser &lt;span class="nt"&gt;--system&lt;/span&gt; ggc_user
&lt;span class="nb"&gt;sudo &lt;/span&gt;addgroup &lt;span class="nt"&gt;--system&lt;/span&gt; ggc_group

&lt;span class="c"&gt;# download the Greengrass core package&lt;/span&gt;
&lt;span class="c"&gt;# this is version 1.10.1 at time of writing&lt;/span&gt;
wget https://d1onfpft10uf5o.cloudfront.net/greengrass-core/downloads/1.10.1/greengrass-linux-aarch64-1.10.1.tar.gz

&lt;span class="c"&gt;# extract the greengrass files to root&lt;/span&gt;
&lt;span class="nb"&gt;sudo tar&lt;/span&gt; &lt;span class="nt"&gt;-xzvf&lt;/span&gt; greengrass-linux-aarch64-1.10.1.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /

&lt;span class="c"&gt;# install the rootCA&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;wget &lt;span class="nt"&gt;-O&lt;/span&gt; /greengrass/certs/root.ca.pem https://www.amazontrust.com/repository/AmazonRootCA1.pem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next we need to extract those Greengrass configuration files we generated in the previous steps. To do this extract the &lt;code&gt;tar.gz&lt;/code&gt; (make sure to change the filename based on your file)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo tar&lt;/span&gt; &lt;span class="nt"&gt;-xzvf&lt;/span&gt; &amp;lt;certificate_id&amp;gt;-setup.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /greengrass

&lt;span class="c"&gt;# Example of my command&lt;/span&gt;
&lt;span class="nb"&gt;sudo tar&lt;/span&gt; &lt;span class="nt"&gt;-xzvf&lt;/span&gt; 551aca43d9-setup.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /greengrass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Optionally&lt;/strong&gt; you can also run the following commands to install some extra dependencies&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;openjdk-8-jdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're now setup and ready to start the Greengrass core service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# start greengrass&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; /greengrass/ggc/core/
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./greengrassd start
&lt;span class="c"&gt;# Setting up greengrass daemon&lt;/span&gt;
&lt;span class="c"&gt;# Validating hardlink/softlink protection&lt;/span&gt;
&lt;span class="c"&gt;# Waiting for up to 1m10s for Daemon to start&lt;/span&gt;

&lt;span class="c"&gt;# Greengrass successfully started with PID: 12319&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, confirm that the service is working properly by running the following (use the PID from the command output above)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux | &lt;span class="nb"&gt;grep&lt;/span&gt; &amp;lt;PID_FROM_ABOVE&amp;gt;
&lt;span class="c"&gt;# root     12319  1.3  0.5 1008696 23308 pts/1   Sl   21:14   0:00 /greengrass/ggc/packages/1.10.1/bin/daemon -core-dir /greengrass/ggc/packages/1.10.1 -greengrassdPid 12314&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Deploy Greengrass Group
&lt;/h4&gt;

&lt;p&gt;The final step is to run the deploy for the Greengrass group that was just setup. To do this, navigate to the &lt;a href="https://console.aws.amazon.com/iot/home?region=us-east-1#/greengrass/grouphub" rel="noopener noreferrer"&gt;Greengrass Groups section of the AWS IoT portal&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Open up the Greengrass group that we just deployed by clicking its name.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fpw5jx9weaoife9ug0u7c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fpw5jx9weaoife9ug0u7c.png" alt="AWS IoT Greengrass Groups"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To run the deployment for the device, simply click &lt;strong&gt;Deploy&lt;/strong&gt; under the &lt;strong&gt;Actions&lt;/strong&gt; menu.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fe63g2tlt770r1hbuxck0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fe63g2tlt770r1hbuxck0.png" alt="AWS IoT Greengrass group deploy"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You will be prompted how you would like to deploy, go with &lt;strong&gt;Automatic detection&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fo6gi5o2fvd7u87rp9pgo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fo6gi5o2fvd7u87rp9pgo.png" alt="AWS IoT Greengrass group automatic detection"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After a minute or so you should see the Greengrass group has been deployed by a green indicator.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffmles2naicib02vejzqq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ffmles2naicib02vejzqq.png" alt="AWS IoT Greengrass group successful deployment"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;Congratulations! You created an AWS IoT Greengrass group that is ready for use on your edge based IoT project. With this Greengrass Core setup on the Jetson Nano you can now start looking at more complex tasks such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lambda Functions on the Edge&lt;/li&gt;
&lt;li&gt;Machine Learning on the Edge&lt;/li&gt;
&lt;li&gt;&lt;a href="https://devopstar.com/2019/12/14/greener-grass-docker-deployments" rel="noopener noreferrer"&gt;Docker containers on Greengrass&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you had any issues setting things up, or you have other questions, please let me know by reaching out on Twitter &lt;a href="https://twitter.com/nathangloverAUS" rel="noopener noreferrer"&gt;@nathangloverAUS&lt;/a&gt; or dropping a comment below.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>iot</category>
      <category>beginners</category>
      <category>greengrass</category>
    </item>
    <item>
      <title>Escaping from technical debt when it seems impossible</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Mon, 01 Jun 2020 15:49:38 +0000</pubDate>
      <link>https://dev.to/t04glovern/escaping-from-technical-debt-when-it-seems-impossible-40om</link>
      <guid>https://dev.to/t04glovern/escaping-from-technical-debt-when-it-seems-impossible-40om</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Please reach out to me on &lt;a href="https://twitter.com/nathangloverAUS" rel="noopener noreferrer"&gt;Twitter @nathangloverAUS&lt;/a&gt; if you have follow up questions!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This post was originally written on &lt;a href="https://devopstar.com/" rel="noopener noreferrer"&gt;DevOpStar&lt;/a&gt;&lt;/em&gt;. Check it out &lt;a href="https://devopstar.com/2020/05/30/escaping-from-technical-debt" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On August 16th, 2019 &lt;a href="https://selfie2anime.com/" rel="noopener noreferrer"&gt;Selfie2Anime went live&lt;/a&gt; to unexpected success. Unfortunately, the code deployed wasn't anywhere near ready for the load we experienced and some bad decisions were made in the heat of the moment to get everything up and running. 8 months on and we're dealing with the consequences of those decisions every day.&lt;/p&gt;

&lt;p&gt;In this post, I tell the tale of how we came to terms with our problems and learnt to deal with them. I will also give provide some advice about how we might do things differently next time around.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; &lt;em&gt;that this post was written from the perspective of a small, part-time team; so it might not translate to large scale organizations. However, I would still advocate that most of the recommendations below are at least worth exploring no matter the circumstance.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Context
&lt;/h3&gt;

&lt;p&gt;Before I get into the weeds it will help if you understand what our service is and &lt;strong&gt;why we believe we might have been doomed to fail from the get go&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://selfie2anime.com/" rel="noopener noreferrer"&gt;Selfie2Anime&lt;/a&gt; uses unsupervised generative networks (&lt;a href="https://github.com/taki0112/UGATIT" rel="noopener noreferrer"&gt;UGATIT&lt;/a&gt;) to generate anime images from human selfies.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fzk9ltfc2zkbjaapzyaye.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fzk9ltfc2zkbjaapzyaye.png" alt="Selfie2Anime - Australia's Prime Ministers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fundamental premise&lt;/strong&gt;: An image is uploaded to our service, processed, then sent back to the user. Pretty simple huh? From a distance this might seem like a pretty standard pattern in terms of hosting, the following architecture is probably what comes to your head.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fz78tompegbgu4w03dp18.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fz78tompegbgu4w03dp18.png" alt="Selfie2Anime - Simple Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is more or less exactly what I had in my head too when I prototyped out a very simple concept back at the start of August 2019.&lt;/p&gt;

&lt;p&gt;But it wasn't that simple, and if you want to know more about the problems and how we solved them, I recommend checking out &lt;a href="https://devopstar.com/2019/08/25/building-selfie2anime-iterating-on-an-idea" rel="noopener noreferrer"&gt;Building Selfie2Anime - Iterating on an Idea&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problems
&lt;/h3&gt;

&lt;p&gt;With the pleasantries out of the way, let's dig into the list of things that ended up being major problems for Selfie2Anime.&lt;/p&gt;

&lt;h4&gt;
  
  
  I'll fix it later syndrome
&lt;/h4&gt;

&lt;p&gt;When working on software projects, especially when they are experiments or proof of concepts; it's not uncommon to write quick and dirty code to prove out functionality. Normally the rationale is along the lines of "I know this could be better, but I'll fix it up later".&lt;/p&gt;

&lt;p&gt;Unsurprisingly this &lt;strong&gt;only works if you actually go back and fix it up later&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Here's a great example of some dud code I wrote when building out the image processing loop. If you've worked with Python before, you'll probably notice a few really bad patterns in the code below.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;fake_img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test_endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;file_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;outgoing/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;
            &lt;span class="n"&gt;image_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fake_img&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmailService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FATAL ERROR&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Single broad &lt;code&gt;Exception&lt;/code&gt; being caught for all errors&lt;/li&gt;
&lt;li&gt;No caching of service clients (email, s3, etc.)&lt;/li&gt;
&lt;li&gt;Errors aren't particularly useful.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A more useful way to write the above code could have been what is shown below (however even this isn't necessarily the best way).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Cache clients in global scope
&lt;/span&gt;&lt;span class="n"&gt;s3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmailService&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;fake_img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test_endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;crop_img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR: Processing image with GAN: {}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;file_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;outgoing/&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;image&lt;/span&gt;
            &lt;span class="n"&gt;image_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;file_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR: Uploading image to S3: {}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ERROR: Failed to send email: {}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;

    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;FATAL ERROR&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The question is, &lt;strong&gt;why wouldn't I write better code the first time around?&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There's nothing wrong with writing bad code; but hold yourself accountable for fixing it later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I found flagging code I wasn't particularly proud of with &lt;code&gt;#TODO&lt;/code&gt;'s helped a lot as a quick and easy way to acknowledge a problem. Then I would make use of IDE extensions like &lt;a href="https://github.com/Gruntfuggly/todo-tree" rel="noopener noreferrer"&gt;Todo Tree&lt;/a&gt; which keeps a running list of code smells and future improvements right in your IDE.&lt;/p&gt;

&lt;p&gt;If you are working with other people who are also touching the code base, then It might also be worthwhile setting up a &lt;a href="https://trello.com/" rel="noopener noreferrer"&gt;Trello&lt;/a&gt; board and jot down the problems there. This helps the rest of your teammates to understand where you might be cutting corners so they are more understanding when you ask for time to clean up code smells at at later date.&lt;/p&gt;

&lt;h4&gt;
  
  
  Have a Backlog
&lt;/h4&gt;

&lt;p&gt;Overtime in both my professional and personal careers I've grown to rely on having a documented backlog of work to work from. During my more junior years, I didn't fully understand why it was important and how it can be a very helpful tool for managing technical debt.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A backlog exists to help you and your teammates better understand your inflight and future workload.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Write some dud code?&lt;/strong&gt; create a task in the backlog to clean it up later&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Running into friction when trying to do something in the codebase?&lt;/strong&gt; throw a card in the backlog to remind you to automate a process when you have some downtime&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Noticing users complaining about a bug?&lt;/strong&gt; Backlog!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1t9py7ecn7fxmz5s9ibv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F1t9py7ecn7fxmz5s9ibv.png" alt="Selfie2Anime Backlog example"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've found that having visibility over tasks and &lt;code&gt;TODO&lt;/code&gt;'s in a central place helps keep you accountable, and lets your teammates know what kind of time might need to be invested in the future to retroactively improve the project.&lt;/p&gt;

&lt;h4&gt;
  
  
  CI/CD is more important then you think (most of the time)
&lt;/h4&gt;

&lt;p&gt;During my day job, I pride myself on the level of work that goes into making sure CI/CD is a first-class citizen. Good CI/CD allows you to move faster and get changes into production more frequently, so it seems like a no-brainer to include it in every project you work on.&lt;/p&gt;

&lt;p&gt;There is an argument that trying to setup end to end CI/CD at the start of a project can be a form of procrastination. If this is a very early stage personal project or &lt;strong&gt;real experimentation&lt;/strong&gt; where you are trying to prove something out as quick as possible, then I would argue that an end to end deployment strategy is overkill.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fsky3abpnhekrfqq95bo5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fsky3abpnhekrfqq95bo5.png" alt="XKCD - Is it worth the time"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You'll usually know when it's time to start setting up a proper deployment pipeline when you are no longer able to make changes regularly, and instead schedule in patches because the process of deploying code has become a hassle.&lt;/p&gt;

&lt;h5&gt;
  
  
  Automated Builds
&lt;/h5&gt;

&lt;p&gt;I advocate that having an automated build for a new project is always worth setting up, provided it isn't a job that takes a few days. Having some simple way to confirm builds succeed when pushing to production not only helps you test code more frequently, but it allows other members working on the same codebase to be able to test things themselves without needing your involvement.&lt;/p&gt;

&lt;p&gt;A good example of this on Selfie2Anime is our docker container builds. It was taking up to 50 minutes to do a full deploy to production as each container build had to include the &lt;strong&gt;7.6gb selfie2anime model&lt;/strong&gt;. For the first few weeks, I powered through this inconvenience because I didn't see it as an important piece of work.&lt;/p&gt;

&lt;p&gt;Over time these builds meant I was &lt;strong&gt;terrified of making changes&lt;/strong&gt; because I knew if I messed something up it would be an hour of downtime.&lt;/p&gt;

&lt;p&gt;It took me 8 months to implement these automated docker builds. I'm not proud to admit this, and the moment I finally got around to adding them I realized how badly the fear of failed builds was affecting the release cycles. Below is the actual code change I had to make to enable the automated builds, &lt;em&gt;note how few lines it ended up taking verses the months of putting up with slow releases&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.2&lt;/span&gt;
&lt;span class="na"&gt;phases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pre_build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mkdir -p checkpoint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Downloading s2a model...&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;aws s3 sync s3://devopstar/resources/ugatit/selfie2anime checkpoint/&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Logging in to Amazon ECR...&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;$(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION)&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Building the Docker image...&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker build -t $IMAGE_REPO_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION .&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker tag $IMAGE_REPO_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION&lt;/span&gt;
  &lt;span class="na"&gt;post_build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo Pushing the Docker image...&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker push $AWS_ACCOUNT_ID.dkr.ecr.$AWS_DEFAULT_REGION.amazonaws.com/$IMAGE_REPO_NAME:$CODEBUILD_RESOLVED_SOURCE_VERSION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another good outcome of setting up this simple automated build was that it gave &lt;a href="https://twitter.com/RicoBeti" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; some context of how the codebase was being built and pushed to production.&lt;/p&gt;

&lt;h4&gt;
  
  
  A second opinion / Code reviews
&lt;/h4&gt;

&lt;p&gt;Code reviews are another practice that outside of my professional career I didn't properly see the value in. A large part of why I didn't see code reviews as important on Selfie2Anime was that I thought full context would be required by the reviewer in order for them to give me meaningful advice.&lt;/p&gt;

&lt;p&gt;For example, &lt;a href="https://twitter.com/RicoBeti" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; hadn't worked on the backend yet so I assumed he wouldn't have the context to make meaningful criticism.&lt;/p&gt;

&lt;p&gt;In reality, his distance from the codebase meant he was able to notice things I had become immune from noticing. A good example of this in action was around the following lines of code that pull messages from a queue and return them for processing&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqs_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receive_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;QueueUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;queue_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;AttributeNames&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;All&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;MaxNumberOfMessages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Messages&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MessageId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ReceiptHandle&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ReceiptHandle&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Messages&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqs_client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_message_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;QueueUrl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;queue_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Entries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Successful&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Failed to delete messages: entries={entries} resp={resp}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://twitter.com/RicoBeti" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; asked why there was a &lt;code&gt;while True&lt;/code&gt; around this code, as the &lt;code&gt;sqs_client.receive_message&lt;/code&gt; function call handled the retrieval of 10 messages for me. The while loop was making the code much more susceptible to error; whereas I looked at it as a way to ensure the application was resilient.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fa6m5dd7d5m78regfz2lj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fa6m5dd7d5m78regfz2lj.png" alt="Code Quality 3 - XKCD"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the end, Rico was right, and having him challenge my perspective means that we now have a much more stable system in place.&lt;/p&gt;

&lt;h5&gt;
  
  
  Recommendation for solo projects
&lt;/h5&gt;

&lt;p&gt;Talk to your co-workers and friends about your code, chances are they'll revel at the opportunity to help. I spoke with &lt;a href="https://twitter.com/jgunnink" rel="noopener noreferrer"&gt;@jgunnink&lt;/a&gt; a lot when I was working on DynamoDB problems (relating to optimally querying information). He made some good comments about why I wasn't using GSI's (global secondary indexes). This sparked me to go investigate what they were and how they worked.&lt;/p&gt;

&lt;p&gt;This simple conversation helped boost my query performance from &amp;gt;1 minute to under a fraction of a second.&lt;/p&gt;

&lt;h4&gt;
  
  
  Sharing access
&lt;/h4&gt;

&lt;p&gt;Selfie2Anime is deployed into an AWS account that I own myself, it unfortunately ended up sharing the estate with some other personal projects as well. This has the annoying side-effect of being a little difficult to give my team access to all the resources, while also keeping the data safe and private.&lt;/p&gt;

&lt;p&gt;We opted to have myself do all the deployments, which initially wasn't a problem because I didn't mind running builds and taking responsibility. However, the outcome of me doing this was that the rest of the team weren't forced to have exposure to the stack and its complexities. This meant that deploying fell entirely on my shoulders, and there was no incentive for my teammate to learn this for themselves.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Sharing responsibility&lt;/strong&gt; through equal access, especially on small projects like this one helps alleviate strain on one person&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The result was to create a tightly scoped AWS IAM Role that would allow &lt;a href="https://twitter.com/RicoBeti" rel="noopener noreferrer"&gt;Rico&lt;/a&gt; into specific resources within my account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;Technical debt has many different faces, not all of them can be hidden within code. Some are simply just &lt;strong&gt;bad takes&lt;/strong&gt; by individuals who think they have the best intentions but need some external clarity.&lt;/p&gt;

&lt;p&gt;I found working on this project helped me better understand when I was &lt;strong&gt;digging a deeper hole&lt;/strong&gt; instead of solving the root cause of my problems, and moving forward I'm excited to kick off new projects with these lessons learnt in mind.&lt;/p&gt;

&lt;p&gt;I would LOVE to hear from anyone who has had to deal with slow growing technical debt, especially on smaller projects. It is difficult to find people wanting to talk about bad experiences; so I encourage you to drop a comment below or hit me up on twitter &lt;a href="https://twitter.com/nathangloverAUS" rel="noopener noreferrer"&gt;@nathangloverAUS&lt;/a&gt;&lt;/p&gt;

</description>
      <category>techdebt</category>
      <category>codequality</category>
      <category>programming</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Internet of Things - Recap (May 2020)</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Thu, 28 May 2020 05:58:30 +0000</pubDate>
      <link>https://dev.to/t04glovern/internet-of-things-recap-may-2020-28fh</link>
      <guid>https://dev.to/t04glovern/internet-of-things-recap-may-2020-28fh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Please reach out to me on &lt;a href="https://twitter.com/nathangloverAUS" rel="noopener noreferrer"&gt;Twitter @nathangloverAUS&lt;/a&gt; if you have follow up questions!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This post was originally written on &lt;a href="https://devopstar.com/" rel="noopener noreferrer"&gt;DevOpStar&lt;/a&gt;&lt;/em&gt;. Check it out &lt;a href="https://devopstar.com/newsletter/2020/05/25" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Welcome to the May 2020 issue of &lt;em&gt;the yet to be officially named&lt;/em&gt; &lt;strong&gt;Internet of Things monthly recap&lt;/strong&gt;. Join me as I take you through some of the interesting things I've seen in the world of IoT this month.&lt;/p&gt;

&lt;h2&gt;
  
  
  Community Picks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://github.com/Fitbit/golden-gate" rel="noopener noreferrer"&gt;Project Golden Gate&lt;/a&gt;
&lt;/h3&gt;




&lt;p&gt;&lt;a href="https://github.com/Fitbit/golden-gate" rel="noopener noreferrer"&gt;Golden Gate&lt;/a&gt; is an open source project released by Fitbit that hopes to simplify communication between embedded devices by providing an IP-based protocol stacks over the top of Bluetooth low energy (BLE).&lt;/p&gt;

&lt;p&gt;The documentation is a bit lacking currently, however there is example code available for &lt;a href="https://github.com/Fitbit/golden-gate/tree/master/platform/android" rel="noopener noreferrer"&gt;Android&lt;/a&gt;, &lt;a href="https://github.com/Fitbit/golden-gate/tree/master/platform/apple" rel="noopener noreferrer"&gt;iOS&lt;/a&gt; and the &lt;a href="https://github.com/Fitbit/golden-gate/tree/master/platform/esp32/gg_peripheral" rel="noopener noreferrer"&gt;ESP32&lt;/a&gt;. There are comments that this framework will be expanded to support other transport layers besides BLE, so perhaps this is a good repo to star and come back to at some point in the future.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://upverter.com/design/maisken/19555d1b5c7ce35a/homelay-v101/" rel="noopener noreferrer"&gt;The Maisken Homelay - ESP32 power up board&lt;/a&gt;
&lt;/h3&gt;




&lt;p&gt;A lot of people would agree that the ESP32 has become the king in terms of home automation. Look at projects like &lt;a href="https://esphome.io/" rel="noopener noreferrer"&gt;ESPHome&lt;/a&gt; that is entirely based around the use of this incredible board. However there are a couple common patterns where the ESP32 often needs extra components for projects; and this is where &lt;a href="https://upverter.com/design/maisken/19555d1b5c7ce35a/homelay-v101/" rel="noopener noreferrer"&gt;The Maisken Homelay&lt;/a&gt; could come in handy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fzz8bqjku9utztqtwklmm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fzz8bqjku9utztqtwklmm.png" alt="ESP32 Maisken Homelay"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Currently the post outlines that the following features are available in the designs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;4 dry-contact relays&lt;/li&gt;
&lt;li&gt;Breakouts to access eight GPIOs, two of which can be analog inputs&lt;/li&gt;
&lt;li&gt;Two input-only pins&lt;/li&gt;
&lt;li&gt;SDA/SCL pins for I2C&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Overall this design will hopefully be useful for home automation systems like sprinklers or appliance controllers that need relays.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://aws.amazon.com/about-aws/whats-new/2020/05/espressifs-esp32-wroom-32se-module-now-qualified-for-aws-iot-multi-account-registration/" rel="noopener noreferrer"&gt;Espressif’s ESP32-WROOM-32SE AWS IoT Multi-Account Registration&lt;/a&gt;
&lt;/h3&gt;




&lt;p&gt;I should just rename this newsletter, the ESP32 newsletter because we have so much of it this month. The ESP32-WROOM-32SE has been qualited for use with AWS IoT Core Multi-Account Registration.&lt;/p&gt;

&lt;p&gt;The chip can integrate with a ATECC608A secure element to store device certificates that could be used for provisioning; simplifying the process of provisioning IoT products into different accounts.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.notebookcheck.net/Lilygo-A-customisable-smartwatch-with-touch-Wi-Fi-and-Arduino-support-that-will-set-you-back-less-than-US-25.466013.0.html" rel="noopener noreferrer"&gt;LilyGO - Smartwatch with touch, Wi-Fi and Arduino support&lt;/a&gt;
&lt;/h3&gt;




&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4svaiaoijl7t3hx7vpyp.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4svaiaoijl7t3hx7vpyp.jpg" alt="TTGO T-Watch-2020"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;LilyGO unveiled its new TTGO T-Watch-2020, a highly customizable smartwatch using an ESP32. The device has the following features according to the product page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1.54 inch capacitive screen (ST7789V)&lt;/li&gt;
&lt;li&gt;350ma LiPo battery&lt;/li&gt;
&lt;li&gt;Triaxial accelerometer (BMA423)&lt;/li&gt;
&lt;li&gt;Vibration motor, speaker and infrared signal sensor&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main selling point of this product is that it's an &lt;a href="https://github.com/Xinyuan-LilyGO/TTGO_TWatch_Library" rel="noopener noreferrer"&gt;open platform&lt;/a&gt; with integration with Scratch, micropython, Arduino and pretty much any other methods you'd normally use to communicate with ESP32 chips.&lt;/p&gt;

&lt;p&gt;While the product itself seems to be totally sold out already &lt;a href="https://www.tindie.com/products/ttgo/lilygor-ttgo-t-watch-2020/" rel="noopener noreferrer"&gt;you can join a queue on the tindie product page&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://aws.amazon.com/blogs/aws/new-price-reduction-for-aws-iot-jobs-globally-available/" rel="noopener noreferrer"&gt;90%+ price reduction for AWS IoT Jobs&lt;/a&gt;
&lt;/h3&gt;




&lt;p&gt;If you are a user of AWS IoT Jobs then good news! The cost of running these are ~90% reduced now.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fc3uld4jv8dcaet82e56c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fc3uld4jv8dcaet82e56c.png" alt="AWS IoT Jobs pricing from AWS blog page"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In the past you might have seen me talk about AWS IoT Jobs being used with OTA (over-the-air) updates for devices or just running small blocks of code across huge fleets of managed IoT devices.&lt;/p&gt;

&lt;p&gt;Either way, this is great news if you're using the service at scale and have noticed the costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  DevOpStar Picks
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://devopstar.com/2020/05/16/aws-iot-esp32-cam-setup" rel="noopener noreferrer"&gt;AWS IoT - ESP32-CAM Setup&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Occasionally while looking for electronics to play around with I come across something that is so cheap I have to assume it's a scam. For me the ESP-32 CAM was one of such devices; sporting Bluetooth, WiFi, SD Card support and a Camera all for ~$7, it really does seem too good to be true. Lucky for us, this outstanding little SoC is definitely real, and is capable of doing great things (if you are willing to dig into code a little).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4ubfk8rflik6ac5px1n6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F4ubfk8rflik6ac5px1n6.jpg" alt="ESP32-CAM Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devopstar.com/2020/05/16/aws-iot-esp32-cam-setup" rel="noopener noreferrer"&gt;In this post&lt;/a&gt; we look at the ESP32-CAM and specifically work to get it running on AWS IoT.&lt;/p&gt;

</description>
      <category>iot</category>
      <category>aws</category>
      <category>news</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Dog Bark Detector - Frontend</title>
      <dc:creator>Nathan Glover</dc:creator>
      <pubDate>Sat, 16 May 2020 11:16:51 +0000</pubDate>
      <link>https://dev.to/t04glovern/dog-bark-detector-frontend-3b15</link>
      <guid>https://dev.to/t04glovern/dog-bark-detector-frontend-3b15</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Please reach out to me on &lt;a href="https://twitter.com/nathangloverAUS"&gt;Twitter @nathangloverAUS&lt;/a&gt; if you have follow up questions!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;em&gt;This post was originally written on &lt;a href="https://devopstar.com/"&gt;DevOpStar&lt;/a&gt;&lt;/em&gt;. Check it out &lt;a href="https://devopstar.com/2020/04/15/dog-bark-detector-frontend"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've always been curious as to what my pets get up to when I'm out of the house. This curiosity finally got the best of me this week and I decided to build I wanted to build a dog bark detection system in order to keep track of how often our two dogs bark.&lt;/p&gt;

&lt;p&gt;This guide covers the second of three guides around how the Dog Bark Detector was built; specifically focusing on the &lt;strong&gt;Frontend&lt;/strong&gt;. You can see the components highlighted below that are core to this section of the guide.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7mKS-yus--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/x7h92fsdixip1uocm0d4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7mKS-yus--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/x7h92fsdixip1uocm0d4.png" alt="Frontend Architecture" width="880" height="586"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It covers the following concepts at a high level along with providing all the source code and resources you will need to create your own version.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AppSync data sourcing from DynamoDB&lt;/li&gt;
&lt;li&gt;Vue Frontend&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Research
&lt;/h3&gt;

&lt;p&gt;You're probably noticing a trend now, but I will always start a project by doing some research for existing code bases or samples available that might have already solved what I am trying to do. This was no exception and I went hunting around for good ways to present the data I've started to accrue.&lt;/p&gt;

&lt;p&gt;Turns out I didn't need to look very far this time though...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DynamoDB AppSync data source&lt;/strong&gt; - Fantastic service that allows you to simply connect an &lt;a href="https://aws.amazon.com/appsync/"&gt;AppSync&lt;/a&gt; (GraphQL) API up to an existing DynamoDB database and have all your access patterns created for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amplify&lt;/strong&gt; frontend - &lt;a href="https://aws.amazon.com/amplify/"&gt;AWS Amplify&lt;/a&gt; is an easy way to integrate your frontend with backend AWS services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vue.js&lt;/strong&gt; - Frontend framework that the amazing &lt;a href="https://twitter.com/RicoBeti"&gt;Rico Beti&lt;/a&gt; knows really well and offered to help me with&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I felt really confident based on the quick research that we'd be able to achieve a simple proof of concept fairly easily based on the tech stack above. In the next section we start implementing it all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating AppSync API
&lt;/h3&gt;

&lt;p&gt;In this section we will start creating our AppSync API. Start by navigating to the &lt;a href="https://ap-southeast-2.console.aws.amazon.com/appsync/home"&gt;AppSync console&lt;/a&gt; and click the &lt;strong&gt;Create API&lt;/strong&gt; button. This will start up a launch wizard where you will be selecting &lt;strong&gt;Import DynamoDB Table&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jhI3ZeCD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/or04hwskz8urxqdxtzih.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jhI3ZeCD--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/or04hwskz8urxqdxtzih.png" alt="AppSync import existing DynamoDB table" width="880" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select the region you table is located in and then select your existing table (should auto-fill). You can either use the auto generator for role or if you know what you're doing; select an existing role that will be used by AppSync to connect to the DynamoDB table.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3rPdGgNW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/cge528hqrynbqy3oo0pd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3rPdGgNW--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/cge528hqrynbqy3oo0pd.png" alt="AppSync import existing DynamoDB table selection" width="736" height="403"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you click import you will be prompted to define the extra schema attributes. For the sake of completeness we will add all the required fields to the list so they are generated in the API for us.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3rLCS-eb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/2zckm2oiwb1si6z5c1c7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3rLCS-eb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/2zckm2oiwb1si6z5c1c7.png" alt="AppSync define DynamoDB schema" width="827" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hit complete and you'll be presented back your new AppSync API ready for use. Before we go using it however, we're going make a couple small changes to the schema so that it only allows &lt;strong&gt;read&lt;/strong&gt; access. Navigate to the &lt;strong&gt;Schema&lt;/strong&gt; tab under your new API and comment out the followin sections&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;COMMENT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;THESE&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CreateBarksInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;deviceId:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;String!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;timestamp:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Int!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;probability:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;String!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;camera:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;String!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;wav_file:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;String!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;UpdateBarksInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;deviceId:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;String!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;timestamp:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Int!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;probability:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;String!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;camera:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;String!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;wav_file:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;String!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;DeleteBarksInput&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;deviceId:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;String!&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;timestamp:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Int!&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Mutation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;createBarks(input:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;CreateBarksInput!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Barks&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;updateBarks(input:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;UpdateBarksInput!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Barks&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="err"&gt;deleteBarks(input:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;DeleteBarksInput!):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Barks&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--J_sY_p7M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/h307fjwmwf0xsvsvm2l7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--J_sY_p7M--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/h307fjwmwf0xsvsvm2l7.png" alt="AppSync Schema changes" width="685" height="534"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click save and validate that you weren't given any syntax errors when changing the Schema. Next we'll move onto connecting up our API in the Vue.js app.&lt;/p&gt;

&lt;h3&gt;
  
  
  Connect AppSync API
&lt;/h3&gt;

&lt;p&gt;The next step is to connect the AppSync API to our frontend app by using AWS Amplify. You might have noticed that when you first imported the DynamoDB instance to AppSync you were given a set of commands to run to integrate AWS Amplify. We're going to be following these steps now in order to do just that.&lt;/p&gt;

&lt;p&gt;I recommend working with our existing repository &lt;a href="https://github.com/t04glovern/dog-bark-detection"&gt;t04glover/dog-bark-detector&lt;/a&gt; by pulling it down locally&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/t04glovern/dog-bark-detection.git
&lt;span class="nb"&gt;cd &lt;/span&gt;dog-bark-detection/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next remove any existing configuration that we have currently added (this is using our personal API).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; amplify
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; src/graphql
&lt;span class="nb"&gt;rm &lt;/span&gt;src/API.ts
&lt;span class="nb"&gt;rm &lt;/span&gt;src/aws-exports.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next follow the instructions from the AppSync page in regards to configuring this Vue.js app with Amplify. Run the following commands within the &lt;code&gt;app&lt;/code&gt; folder of the project.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XYjt4KxI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/npjc6k2vewg3aos9w4gv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XYjt4KxI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/npjc6k2vewg3aos9w4gv.png" alt="AppSync Amplify setup example" width="802" height="439"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @aws-amplify/cli
amplify init
&lt;span class="c"&gt;# ? Enter a name for the project bark-detector&lt;/span&gt;
&lt;span class="c"&gt;# ? Enter a name for the environment dev&lt;/span&gt;
&lt;span class="c"&gt;# ? Choose your default editor: Visual Studio Code&lt;/span&gt;
&lt;span class="c"&gt;# ? Choose the type of app that you're building javascript&lt;/span&gt;
&lt;span class="c"&gt;# Please tell us about your project&lt;/span&gt;
&lt;span class="c"&gt;# ? What javascript framework are you using vue&lt;/span&gt;
&lt;span class="c"&gt;# ? Source Directory Path:  src&lt;/span&gt;
&lt;span class="c"&gt;# ? Distribution Directory Path: dist&lt;/span&gt;
&lt;span class="c"&gt;# ? Build Command:  npm run-script build&lt;/span&gt;
&lt;span class="c"&gt;# ? Start Command: npm run-script serve&lt;/span&gt;

amplify add codegen &lt;span class="nt"&gt;--apiId&lt;/span&gt; &amp;lt;API_ID_FROM_YOUR_OWN_PROJECT&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;NOTE&lt;/strong&gt;: When prompted, allow it to generate all the possible API's for you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then within the actual Vue.js code in &lt;code&gt;app/src/store/app.ts&lt;/code&gt; you are able to see where we are importing the Amplify specific code to setup authentication&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Amplify&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;graphqlOperation&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;aws-amplify&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;listBarks&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/graphql/queries&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;awsconfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@/aws-exports&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;Amplify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;awsconfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later on in that same file we call the listBarks GraphQL operation like so in order to pull 500 records and push them into the list of items on the frontend&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allBarks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;API&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;graphql&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;graphqlOperation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;listBarks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detections&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;IDetection&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allBarks&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;listBarks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;IDetection&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;deviceId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;camera&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;certainty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;probability&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;audioUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;wav_file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;rating&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;setDetections&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;detections&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the app by running the following commands within the &lt;code&gt;app&lt;/code&gt; folder of the project.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open up your browser to &lt;a href="http://localhost:8080/"&gt;http://localhost:8080/&lt;/a&gt; to bask in the glory of the working app&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--bNcgZn7F--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/hznuwj3siqluofppr1dq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--bNcgZn7F--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/hznuwj3siqluofppr1dq.png" alt="Bark Detector frontend app" width="880" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Bonus Graphs
&lt;/h4&gt;

&lt;p&gt;You might have also noticed that there is a &lt;strong&gt;Dashboard&lt;/strong&gt; tab on the left. If you open this up you should be able to see a trend of the last weeks worth of data displayed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ibyY6jqy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/u632ilaojmai1boajfyl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ibyY6jqy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/u632ilaojmai1boajfyl.png" alt="Bark Detector frontend graph" width="880" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Conclusion
&lt;/h3&gt;

&lt;p&gt;This concludes the final part of this guide where we've achieved our goal of creating a simple Dog Bark frontend using Vue.js, Amazon AppSync and AWS Amplify.&lt;/p&gt;

&lt;p&gt;If you have any questions want to show off how you've changed the frontend to be more interesting; please hit me up on &lt;a href="https://twitter.com/nathangloverAUS"&gt;Twitter @nathangloverAUS&lt;/a&gt;&lt;/p&gt;

</description>
      <category>serverless</category>
      <category>aws</category>
      <category>amplify</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
