<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Barney Parker</title><link>https://barneyparker.com/</link><description>Recent content on Barney Parker</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>This work is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.</copyright><lastBuildDate>Thu, 23 Apr 2026 11:56:39 +0000</lastBuildDate><atom:link href="https://barneyparker.com/index.xml" rel="self" type="application/rss+xml"/><item><title>Organization Terraform State Buckets</title><link>https://barneyparker.com/posts/organization-terraform-state-buckets/</link><pubDate>Thu, 23 Apr 2026 11:56:39 +0000</pubDate><guid>https://barneyparker.com/posts/organization-terraform-state-buckets/</guid><description>&lt;h1 id="organization-terraform-state-buckets">Organization Terraform State Buckets&lt;/h1>
&lt;p>Terraform requires a backend to store state files. These are crucial to allow Terraform to know how to map real-world resources to code definition.&lt;/p>
&lt;p>Over the years i&amp;rsquo;ve seen it done an awful lot of ways, and most of them tend to be less than ideal at some time or another. In this post i&amp;rsquo;ll share my preferred way of doing this and the rationale behind it.&lt;/p>
&lt;h1 id="the-problem">The Problem&lt;/h1>
&lt;p>When you initialize a Terraform project, it needs to create the state file. When you;re first learning and haven&amp;rsquo;t provided a &lt;code>backend&lt;/code> config, Terraform will just create a local file called &lt;code>terraform.tfstate&lt;/code> in the same directory as your code. This is fine for learning, but it definitely wont be long until something goes wrong. either you accidentally delete the file and terraform is trying to depoy resources that conflict with things that already exist.&lt;/p>
&lt;p>Often the next problem is multiple people start working on the code. Its just a text file and so teams sometimes think storing the statefile in Git might be a good idea, except that it isn&amp;rsquo;t. Terraform state files can contain sensitive information, and they also change frequently, which can lead to merge conflicts and other issues when stored in version control.&lt;/p>
&lt;p>Thats when teams learn about remote back ends.&lt;/p>
&lt;p>You can have a few different backend types, but i work almost exclusively with AWS, so the natural choice is S3. Create a bucket in your account, and then tell terraform to use it as a backend. For a lot of cases, this is fine, but you have to remember to configure everything correctly, for example make sure its not public, and make sure versioning is enabled. Its not a huge issue, but its one more thing to remember, and its something sat right in the account that should only be hosting the application.&lt;/p>
&lt;p>When things get a little bigger, you create an AWS Organization. now you have multiple accounts, each with a state bucket, and you have to remember to configure each one correctly. Its not hard to create a simple CloudFormation StackSet that will create this in an account by default.&lt;/p>
&lt;p>But I try my absolute best to stay away from CloudFormation unless i have no other choice. There is also the posibility an account wont need a state bucket, so now i need exclusion logic in my stack set and now we&amp;rsquo;re going down another messy, unmaintainable road&amp;hellip;.&lt;/p>
&lt;h1 id="the-solution">The Solution&lt;/h1>
&lt;p>In a well built organization you essentially divide all accounts into a small number top-level Organization Units (OUs). You might have &lt;code>Core&lt;/code> for centralized components, &lt;code>Shared&lt;/code> for shared services, &lt;code>Workloads&lt;/code> for applications and so on.&lt;/p>
&lt;p>I tend to create an account that contains deployment related components, so i imaginatively call it &lt;code>Deployment&lt;/code>. In here I create the One True State Bucket. This bucket is used by all accounts in the organization to store their Terraform state files. This way, I only have to configure one bucket, and I can ensure that it is configured correctly with versioning and encryption.&lt;/p>
&lt;p>But we hit on a small problem. If all accounts are using the same bucket, how do we prevent conflicts? The answer is to use a bucket policy that controls &lt;em>how&lt;/em> the bucket can be accessed.&lt;/p>
&lt;h1 id="the-rules">The Rules&lt;/h1>
&lt;h2 id="org-only-access">Org Only Access&lt;/h2>
&lt;p>The first rule is that only Principals from our organization can access the bucket. This is done by starting with a deny statement that denies all actions where the Principal (the requestor) has the Org ID we expect. For this we use a Global Condition Context Key: &lt;code>aws:PrincipalOrgID&lt;/code>&lt;/p>
&lt;p>This gives us:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;Sid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;DenyNonOrgPrincipals&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Effect&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Deny&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Action&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Resource&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/*&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="nt">&amp;#34;Condition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;StringNotEquals&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;aws:PrincipalOrgID&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;o-1234567890&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="deployment-role-access-only">Deployment Role Access Only&lt;/h2>
&lt;p>Next, we dont want just &lt;em>anyone&lt;/em> to access this. You only want your deployment roles to be able to access this bucket. You are only allowing CI/CD to deploy aren&amp;rsquo;t you????&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;Sid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;DenyNonDeploymentRoles&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Effect&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Deny&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Action&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;s3:*&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="nt">&amp;#34;Resource&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/*&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="nt">&amp;#34;Condition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;StringNotLike&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;aws:PrincipalARN&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:iam::*:role/Deploy-*&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>This policy says that only roles that start with &lt;code>Deploy-&lt;/code> can access the bucket. This way, we can ensure that only our deployment roles can access the state files, and we can prevent any accidental access from other roles. You can change that role name to whatever you want, but the point is that you should have a naming convention for your deployment roles, and then use that convention to control access to the bucket. Deployment roles are one of the few good use-cases for a CloudFormation StackSet in my opinion, so you can use that to create the roles in each account, and then use this policy to control access to the bucket.&lt;/p>
&lt;h2 id="deployment-roles-can-read-anything">Deployment Roles can Read Anything&lt;/h2>
&lt;p>Terraform has a concent of the &lt;code>remote-state&lt;/code> block. This lets you read output from one stack and use it in another and its incredibly useful. For example, you might have a &lt;code>network&lt;/code> stack that creates your VPC and subnets, and then you want to use the output of that stack in your &lt;code>application&lt;/code> stack to create your EC2 instances. With the &lt;code>remote-state&lt;/code> block, you can read the output of the &lt;code>network&lt;/code> stack and use it in your &lt;code>application&lt;/code> stack without having to hardcode any values.&lt;/p>
&lt;p>Because of this, we want to be able to read from any state file, but again, only if its a depoyment role, so we can add this statement:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;Sid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;AllowDeploymentRolesRead&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Effect&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Allow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Action&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;s3:GetObject&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="nt">&amp;#34;Resource&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/*&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="nt">&amp;#34;Condition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;StringLike&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;aws:PrincipalARN&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:iam::*:role/Deploy-*&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>This just ensures &lt;code>GetObject&lt;/code> is allowed for deployment roles, but no other actions are allowed. This way, we can ensure that our deployment roles can read from any state file, but they cant write to any state file that they dont own.&lt;/p>
&lt;h2 id="formatted-keys-for-writing-state">Formatted Keys for Writing State&lt;/h2>
&lt;p>This final statement lets our roles write to the bucket, but to ensure one team cant accidentally over-write another teams state, we add some restrictions.&lt;/p>
&lt;ul>
&lt;li>The Key must identify the account that is trying to write to the state file. We can get this from the IAM variable &lt;code>aws:PrincipalAccount&lt;/code>.&lt;/li>
&lt;li>The key must end with &lt;code>.tfstate&lt;/code> to try to enforce teams to use the standard naming convention.&lt;/li>
&lt;li>The key must contain the project name - sometimes there are multiple projects or stacks deployed to one account&lt;/li>
&lt;li>It must identify the environment, e.g. Dev, Test, Prod etc.&lt;/li>
&lt;/ul>
&lt;p>So our key must look like:&lt;/p>
&lt;p>&lt;code>/&amp;lt;account id&amp;gt;/&amp;lt;project name&amp;gt;/&amp;lt;dev|test|prod&amp;gt;/&amp;lt;stack name&amp;gt;.tfstate&lt;/code>&lt;/p>
&lt;p>This leaves us with this policy:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;Sid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;AllowDeploymentRolesWrite&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Effect&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Allow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Action&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;s3:PutObject&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="nt">&amp;#34;Resource&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/${aws:PrincipalAccount}/*/dev/*.tfstate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/${aws:PrincipalAccount}/*/test/*.tfstate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/${aws:PrincipalAccount}/*/prod/*.tfstate&amp;#34;&lt;/span>
&lt;span class="p">],&lt;/span>
&lt;span class="nt">&amp;#34;Condition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;StringLike&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;aws:PrincipalARN&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:iam::*:role/Deploy-*&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="the-full-policy">The Full Policy:&lt;/h1>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;Version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2012-10-17&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Statement&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;Sid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;EnforceOrgOwnership&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Effect&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Deny&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Action&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;s3:*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Resource&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/*&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="nt">&amp;#34;Condition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;StringNotEquals&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;aws:PrincipalOrgID&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;o-xxxxxxxxxx&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;Sid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;RestrictToDeployRoles&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Effect&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Deny&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Action&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;s3:GetObject&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;s3:PutObject&amp;#34;&lt;/span>&lt;span class="p">],&lt;/span>
&lt;span class="nt">&amp;#34;Resource&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Condition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;StringNotLike&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;aws:PrincipalArn&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:iam::*:role/Deploy-*&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;Sid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;AllowScopedWriteAccess&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Effect&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Allow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Action&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;s3:PutObject&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Resource&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/${aws:PrincipalAccount}/*/dev/*.tfstate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/${aws:PrincipalAccount}/*/test/*.tfstate&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/${aws:PrincipalAccount}/*/prod/*.tfstate&amp;#34;&lt;/span>
&lt;span class="p">],&lt;/span>
&lt;span class="nt">&amp;#34;Condition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;StringLike&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;aws:PrincipalArn&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:iam::*:role/Deploy-*&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;Sid&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;AllowGlobalReadAccessForDeployRoles&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Effect&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Allow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Action&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;s3:GetObject&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Resource&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:s3:::YOUR-BUCKET-NAME/*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;Condition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;StringLike&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;aws:PrincipalArn&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:iam::*:role/Deploy-*&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>In this post, we have discussed how to organize Terraform state buckets in a way that is secure and maintainable. By using a single bucket for all accounts in the organization and controlling access with bucket policies, we can ensure that our state files are protected and that only authorized roles can access them. This approach simplifies management and reduces the risk of misconfiguration while still allowing for the necessary flexibility in accessing state files across different projects and environments.&lt;/p>
&lt;p>This bucket can be backed up, and managed by a limited group of people, and you can be confident that your state files are safe and secure.&lt;/p></description></item><item><title>WiFi QR Codes</title><link>https://barneyparker.com/posts/wifi-qr-codes/</link><pubDate>Mon, 20 Apr 2026 08:00:00 +0000</pubDate><guid>https://barneyparker.com/posts/wifi-qr-codes/</guid><description>&lt;p>&lt;qr-wifi>&lt;/qr-wifi>&lt;/p></description></item><item><title>Backoff Simulator</title><link>https://barneyparker.com/posts/backoff-simulator/</link><pubDate>Thu, 16 Apr 2026 08:00:00 +0000</pubDate><guid>https://barneyparker.com/posts/backoff-simulator/</guid><description>&lt;p>&lt;retry-simulator>&lt;/retry-simulator>&lt;/p></description></item><item><title>Token Bucket Rate Limiter</title><link>https://barneyparker.com/posts/token-bucket-rate-limiter/</link><pubDate>Fri, 10 Apr 2026 08:00:00 +0000</pubDate><guid>https://barneyparker.com/posts/token-bucket-rate-limiter/</guid><description>&lt;h1 id="token-bucket-rate-limiter">Token Bucket Rate Limiter&lt;/h1>
&lt;h2 id="standard-rate-limiting">Standard Rate Limiting&lt;/h2>
&lt;p>A common requirement for many APIs is to limit the number of requests that can be made per unit of time. This can be to prevent abuse, to ensure fair usage, to differentiate between different tiers of users, or to simply prevent overloading the system.&lt;/p>
&lt;p>Rate limiting in itself is a simple approach - - you set a limit of X requests per Y time period, and if a user exceeds that limit, you block them until the time period has passed. This works great when all your users make requests at a consitent frequency, however in most cases users tend to need to make requests in short bursts. Setting a fixed limit in this way means they have a limit when they need o get work done, but most of the time they&amp;rsquo;re wasting available transactions that they could be using.&lt;/p>
&lt;h2 id="token-buckets">Token Buckets&lt;/h2>
&lt;p>Token buckets are a more flexible approach. They still set an average rate limit, but they allow for limits to be exceeded for a time, as long as the average rate is maintained. This is done by using a &amp;ldquo;bucket&amp;rdquo; that fills with &amp;ldquo;tokens&amp;rdquo; at a certain rate. Each time a request is made, a token is removed from the bucket. If there are no tokens left, the request is blocked until a token becomes available.&lt;/p>
&lt;p>For example, if you have a token bucket that fills at a rate of 1 token per second, and has a capacity of 10 tokens, then you can make up to 10 requests in quick succession, but after that, you will need to wait for the bucket to refill before you can make more requests. This allows for bursts of activity while still enforcing an average rate limit.&lt;/p>
&lt;h2 id="combination-token-bucket-rate-limiting">Combination Token Bucket Rate Limiting&lt;/h2>
&lt;p>The problem with a pure token bucket approach is that users can burst, but once their tokens are exceeded, they are limited to an extremely slow rate, e.g. 1 request per second.&lt;/p>
&lt;p>A middle ground is to always ensure an absolute minimum number of tokens are available, e.g. 10 tokens, but then also have a token bucket that fills at a certain rate, e.g. 1 token per second with a maximum of e.g. 100 tokens. This allows for bursts of up to 100 requests, but ensures that even if the bucket is empty, users can still make 10 requests per second.&lt;/p>
&lt;h2 id="implementation">Implementation&lt;/h2>
&lt;p>To implement this we dont need to keep track of every request, we just need to keep track of the number of tokens in the bucket and the last time it was updated. We&amp;rsquo;re going to need a database record.&lt;/p>
&lt;p>We have to consider that a new user wont have a record, and we dont want a table full of records relating to users that either never made a request, or made a request a long time ago and are no longer active. To solve this, we can use a &amp;ldquo;lazy&amp;rdquo; approach to creating records. When a user makes a request, we check if they have a record. If they dont, we create one with the default number of tokens and the current time as the last updated time. If they do have a record, we calculate how many tokens should have been added since the last update, add those to the bucket (up to the maximum), and then check if there are enough tokens to allow the request.&lt;/p>
&lt;p>I am going to show this implementation using AWS DynamoDB which has a TTL feature that allows us to automatically delete records after a period we specify. This doesnt cost anything and happens auotmatically.&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">DynamoDBClient&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;@aws-sdk/client-dynamodb&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">DynamoDBDocumentClient&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">UpdateCommand&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;@aws-sdk/lib-dynamodb&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">client&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">new&lt;/span> &lt;span class="nx">DynamoDBClient&lt;/span>&lt;span class="p">({});&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">doc&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">DynamoDBDocumentClient&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">from&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">client&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="nx">async&lt;/span> &lt;span class="kd">function&lt;/span> &lt;span class="nx">checkRateLimit&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">userId&lt;/span>: &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">nowSec&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Date&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">now&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="mi">1000&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">ONE_DAY_IN_SEC&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">24&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">60&lt;/span> &lt;span class="o">*&lt;/span> &lt;span class="mi">60&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">ttlValue&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nb">Math&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">floor&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">nowSec&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="nx">ONE_DAY_IN_SEC&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">REFILL_RATE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">10&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// 10 tokens per second
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">MAX_BURST&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">100&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// Maximum tokens in the bucket
&lt;/span>&lt;span class="c1">&lt;/span>
&lt;span class="k">try&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">result&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">await&lt;/span> &lt;span class="nx">doc&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">send&lt;/span>&lt;span class="p">(&lt;/span>
&lt;span class="k">new&lt;/span> &lt;span class="nx">UpdateCommand&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">TableName&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;UserRateLimits&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">Key&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">userId&lt;/span> &lt;span class="p">},&lt;/span>
&lt;span class="c1">// We add &amp;#39;ttl&amp;#39; to the SET clause
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="nx">UpdateExpression&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`
&lt;/span>&lt;span class="sb"> SET lastRefill = :now,
&lt;/span>&lt;span class="sb"> ttl = :ttl,
&lt;/span>&lt;span class="sb"> tokens = (
&lt;/span>&lt;span class="sb"> if_not_exists(tokens, :max) +
&lt;/span>&lt;span class="sb"> ((:now - if_not_exists(lastRefill, :now)) * :rate)
&lt;/span>&lt;span class="sb"> ) - :cost
&lt;/span>&lt;span class="sb"> `&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">ConditionExpression&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`(if_not_exists(tokens, :max) + ((:now - if_not_exists(lastRefill, :now)) * :rate)) &amp;gt;= 1`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">ExpressionAttributeValues&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s1">&amp;#39;:now&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">nowSec&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s1">&amp;#39;:rate&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">REFILL_RATE&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s1">&amp;#39;:max&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">MAX_BURST&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s1">&amp;#39;:ttl&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">ttlValue&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">ReturnValues&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;UPDATED_NEW&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">}),&lt;/span>
&lt;span class="p">);&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">allowed&lt;/span>: &lt;span class="kt">true&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">remaining&lt;/span>: &lt;span class="kt">result.Attributes?.tokens&lt;/span> &lt;span class="p">};&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="k">catch&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>: &lt;span class="kt">any&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">err&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">name&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;ConditionalCheckFailedException&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">allowed&lt;/span>: &lt;span class="kt">false&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">message&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Rate limit exceeded&amp;#39;&lt;/span> &lt;span class="p">};&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="k">throw&lt;/span> &lt;span class="nx">err&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>When a request comes in from a user we can call &lt;code>checkRateLimit(userId)&lt;/code> to check if they are allowed to make the request. If they are allowed, we can proceed with processing the request. If they are not allowed, we can return a response indicating that they have exceeded their rate limit.&lt;/p>
&lt;p>Using an Update statement with a &lt;code>ConditionExpression&lt;/code> allows us to atomically check the number of tokens and update the record in a single operation, which is important for ensuring that we don&amp;rsquo;t have race conditions where multiple requests from the same user could potentially allow them to exceed their rate limit.&lt;/p>
&lt;p>We use &lt;code>if_not_exists&lt;/code> to handle the case where a user does not have a record yet. This allows us to create a new record with the default number of tokens and the current time as the last updated time when they make their first request.&lt;/p>
&lt;h2 id="active-user-example">Active User Example&lt;/h2>
&lt;p>An active user will already have a record in the database. When they make a request, we will calculate how many tokens to add based on the time since the last update, add those to the bucket (up to the maximum), and then check if there are enough tokens to allow the request. If there are enough tokens, we will extract one token and update the record with the new number of tokens, the current time as the last updated time, and set the TTL to be 24 hours from now.&lt;/p>
&lt;p>The record will look something like this:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;userId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">95&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;lastRefill&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1620000000&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;ttl&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1620086400&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>If a new request comes in for &lt;code>user123&lt;/code>, and the timestamp is &lt;code>1620000005&lt;/code>, we would calculate the number of tokens to add as &lt;code>(nowSec - lastRefill) * REFILL_RATE&lt;/code> which with values is:&lt;code>(1620000005 - 1620000000) * 10 = 50&lt;/code>. Given that we already had 95 tokens we now have 145 tokens, however, we have a maximum burst of 100 tokens. We then extract one token giving us 99 tokens remaining. We also update the &lt;code>lastRefill&lt;/code> to the current time and set the &lt;code>ttl&lt;/code> to be 24 hours from now.&lt;/p>
&lt;p>We will essentially attempt to create a new record that looks like this:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;userId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;user123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">99&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;lastRefill&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1620000005&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;ttl&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1620086405&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="new-or-inactive-user-example">New or Inactive User Example&lt;/h2>
&lt;p>An Inactive or New user will not have a record in the database. When they make a request, we will use &lt;code>if_not_exists&lt;/code> to create a new record with the default number of tokens (which is the maximum burst) and the current time as the last updated time. We will then extract one token for the request and set the TTL to be 24 hours from now.&lt;/p>
&lt;p>We will default them to the maximum burst of tokens, which in this case is 100 tokens. We will then extract one token for the request, giving them 99 tokens remaining. We will also set the &lt;code>lastRefill&lt;/code> to the current time and set the &lt;code>ttl&lt;/code> to be 24 hours from now.&lt;/p>
&lt;p>The record will look something like this:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;userId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;User123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">99&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;lastRefill&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1620000005&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;ttl&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1620086405&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="alternative-options">Alternative Options&lt;/h2>
&lt;p>In our example we automatically give a user the maximum burstable amount of tokens. This may not be desirable in all cases. It may be that we have an initial balance. We can solve this with this change:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-typescript" data-lang="typescript">&lt;span class="p">.&lt;/span>
&lt;span class="p">.&lt;/span>
&lt;span class="p">.&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">INIITAL_BALANCE&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">;&lt;/span> &lt;span class="c1">// Initial tokens for new users
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="p">.&lt;/span>
&lt;span class="p">.&lt;/span>
&lt;span class="p">.&lt;/span>
&lt;span class="nx">ConditionExpression&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="sb">`(if_not_exists(tokens, :initial) + ((:now - if_not_exists(lastRefill, :now)) * :rate)) &amp;gt;= 1`&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">ExpressionAttributeValues&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="p">.&lt;/span>
&lt;span class="p">.&lt;/span>
&lt;span class="p">.&lt;/span>
&lt;span class="s1">&amp;#39;:initial&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">INIITAL_BALANCE&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">.&lt;/span>
&lt;span class="p">.&lt;/span>
&lt;span class="p">.&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>This way a new record gets created with an initial balance of 50 tokens, and then refills at the same rate as before. This allows us to control the initial burst that a new user can have, while still allowing them to refill their tokens over time.&lt;/p>
&lt;h2 id="real-world-examples">Real World Examples&lt;/h2>
&lt;p>This algorithm is used in the AWS EC2 service for burstable instances. These instances have a baseline performance level, but can burst above that level for short periods of time. The token bucket algorithm is used to determine how much burst capacity an instance has available at any given time, and to ensure that it does not exceed its limits. If you launch a T series instance, you get a certain number of CPU credits that allow you to burst above the baseline performance level. As you use those credits, they are consumed, and if you run out of credits, your instance will be limited to the baseline performance level until you earn more credits over time. This can be seen in a metric called &lt;code>CPUCreditBalance&lt;/code> which shows how many credits you have remaining. If you have a high CPU usage and a low credit balance, you may see a drop in performance as your instance is limited to the baseline level until you earn more credits.&lt;/p></description></item><item><title>Sunlight Lamp</title><link>https://barneyparker.com/posts/sunlight-lamp/</link><pubDate>Wed, 01 Apr 2026 08:00:00 +0000</pubDate><guid>https://barneyparker.com/posts/sunlight-lamp/</guid><description>&lt;h1 id="introduction">Introduction&lt;/h1>
&lt;p>Twice a year it happens. Daylight savings puts the time either one hour ahead, or one hour back. I have a stressfull next week trying to remember how to set the time on my sunlight alarm!&lt;/p>
&lt;p>I find it really hard to wake up in the dark. Many years ago my wife bought me a sunlight alarm and it changed my life! The problem is, i&amp;rsquo;ve long lost the instructions and it doesnt have anything on it to say what it is.&lt;/p>
&lt;p>To make matters worse, this is one of those things that for some reason i just cannot seem to commit to memory! I finally decided that i needed to spend some time working out exactly how to do everything and write myself some more permanent instructions and commit them to Git so i can modify them if i am wrong, and witha bit of luck never actually lose them!&lt;/p>
&lt;h2 id="the-clock">The Clock&lt;/h2>
&lt;p>The clock does a few different things. The most important part is the clock. Its a fairly basic digital clock in a dim orange. On the right-hand edge there is a button marked &lt;code>Dimmer | Time Set&lt;/code> which cycles between &lt;code>bright&lt;/code>, &lt;code>dim&lt;/code>, &lt;code>off&lt;/code>&lt;/p>
&lt;p>The clock &lt;em>cannot&lt;/em> be set while the alarm light is on.&lt;/p>
&lt;p>The clock &lt;em>can&lt;/em> be set when the alarm light is off by holding &lt;code>Dimmer | Time Set&lt;/code> for a few seconds. It then works through the following settings which can be adjusted by pressing the &lt;code>-&lt;/code>, &lt;code>OK&lt;/code>, &lt;code>+&lt;/code> buttons on the edge at the 11:00 position:&lt;/p>
&lt;ul>
&lt;li>&lt;code>24h&lt;/code> | &lt;code>12h&lt;/code>&lt;/li>
&lt;li>Hour&lt;/li>
&lt;li>Minute&lt;/li>
&lt;/ul>
&lt;h2 id="the-alarms">The Alarms&lt;/h2>
&lt;p>The clock supports two alarms which can be turned on and off individually by pressing the &lt;code>🔔1&lt;/code> or &lt;code>🔔2&lt;/code> buttons on the edge at the 9:00 an 10:00 positions. An alarm is active (i.e. not disabled) when their icon shows to the left of the clock&lt;/p>
&lt;p>The alarms have a start time. The light will begin at the lowest brightness level and will increase to the Max Brightness setting over the course of the Light Time setting.&lt;/p>
&lt;p>Alarms can be snoozed by pressing the &lt;code>💤&lt;/code> button on the edge at the 00:00 position.&lt;/p>
&lt;p>The alrams can be set by holding the chosen alarm button for a few seconds. As with the clock, the following settings can be adjusted by pressing the &lt;code>-&lt;/code>, &lt;code>OK&lt;/code>, &lt;code>+&lt;/code> buttons on the edge at the 11:00 position:&lt;/p>
&lt;ul>
&lt;li>Hour&lt;/li>
&lt;li>Minute&lt;/li>
&lt;li>Sound ♪:
&lt;ul>
&lt;li>&lt;code>Off&lt;/code>&lt;/li>
&lt;li>&lt;code>S-01&lt;/code> - Bird Song&lt;/li>
&lt;li>&lt;code>S-02&lt;/code> - Waves&lt;/li>
&lt;li>&lt;code>S-03&lt;/code> - Beeping Alarm&lt;/li>
&lt;li>&lt;code>S-04&lt;/code> - High Piano &amp;amp; Birds&lt;/li>
&lt;li>&lt;code>S-05&lt;/code> - Low Piano&lt;/li>
&lt;li>&lt;code>S-06&lt;/code> - Synth music&lt;/li>
&lt;li>&lt;code>S-07&lt;/code> - Wind Chimes&lt;/li>
&lt;li>&lt;code>FM Radio&lt;/code> - Last chosen station (See Radio)[## Radio]&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Volume (If Sound not set to &lt;code>Off&lt;/code>):
&lt;ul>
&lt;li>&lt;code>U-1&lt;/code> - Very Quiet&lt;/li>
&lt;li>&lt;code>U-20&lt;/code> - Very Load&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>Light Time - Time the light should be on for from the start time set previously&lt;/li>
&lt;li>Max Brightness
&lt;ul>
&lt;li>&lt;code>L-1&lt;/code> barely visible&lt;/li>
&lt;li>&lt;code>L-20&lt;/code> is extremely bright&lt;/li>
&lt;/ul>
&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>Note: In testing, the sound didnt seem to happen? really not sure how that works, but i dont use it, so don&amp;rsquo;t really care too much&lt;/p>
&lt;/blockquote>
&lt;h2 id="sleep">Sleep&lt;/h2>
&lt;p>There is a Sleep setting which plays sounds that gradually get quieter, and can&lt;/p>
&lt;ul>
&lt;li>&lt;code>S-01&lt;/code> - gently &amp;ldquo;aaah&amp;rdquo; sounds&lt;/li>
&lt;li>&lt;code>S-02&lt;/code> - birds&lt;/li>
&lt;li>&lt;code>S-03&lt;/code> - Waves&lt;/li>
&lt;li>&lt;code>Radio&lt;/code> - last station set&lt;/li>
&lt;/ul>
&lt;p>You can set a volume, time and colour, but again, I dont use this so not really sure.&lt;/p>
&lt;h2 id="radio">Radio&lt;/h2>
&lt;p>Ah, who cares. It only does FM and there are only a few FM stations left. If there is a zombie apocalypse, i&amp;rsquo;ll work it out then, assuming there is power&amp;hellip;&amp;hellip;&lt;/p></description></item><item><title>Storage First Design</title><link>https://barneyparker.com/posts/storage-first-design/</link><pubDate>Mon, 30 Mar 2026 17:00:00 +0000</pubDate><guid>https://barneyparker.com/posts/storage-first-design/</guid><description>&lt;h1 id="what-is-storage-first-design">What is Storage First Design?&lt;/h1>
&lt;p>Storage-first is a method of ensuring resilience and scalability in a system. Its a way to ensure a system never loses data, and can always recover from failure. It’s a way to make sure that your system can handle any amount of data, and give itself breathing room to scale up when needed.&lt;/p>
&lt;p>It sounds like a pretty basic idea, but its actually a really powerful way to design systems. By making sure that your system is designed around storage, you can ensure that it can handle any amount of data, and that it can recover from failure without losing anything. It changes a few common approaches to the way a system works, but in such a small way its often overlooked. It can be the difference between a system that can handle a few hundred users, and one that can handle millions.&lt;/p>
&lt;h1 id="storage-vs-process-first">Storage vs Process First&lt;/h1>
&lt;p>Lets think about a theoretical system with and without storage first.&lt;/p>
&lt;p>The system is a payment step in an ecommerce system. At the point this service is invoked, the user has already added items to their cart, and is ready to check out. The system is responsible for taking the payment details, processing the payment, and then returning a response to the user.&lt;/p>
&lt;h2 id="process-first-design">Process First Design&lt;/h2>
&lt;p>In a Process-First approach, we make one call to our back-end and attempt to get a reponse to our user.&lt;/p>
&lt;p>The browser makes an HTTP POST request to /process-payment. The service takes the payment details, processes the payment, and then returns a response to the user. If the payment is successful, it returns a success response. If the payment fails, it returns a failure response.&lt;/p>
&lt;p>The problem is we dont own the actual card processing step. We have to call out to a third party service to process the payment. If that service is down, or if there is a network issue, then our service will fail. The user will get an error message, and they will have to try again later. We might even lose the payment details if we dont have a way to store them.&lt;/p>
&lt;h2 id="storage-first-design">Storage-First Design&lt;/h2>
&lt;p>With storage first design, we would do this in two steps. The browser makes an HTTP POST request to /process-payment. The service takes the payment details, and stores them in a database. It then returns a 202 (Accepted) response with a unique ID for the payment. Ideally the unique ID is generated as some kind of hash of the transaction details. This makes idempotency much easier. If an additional request comes in and has the same hash, we can just return the same response without having to worry about duplicate payments.&lt;/p>
&lt;p>Then we have a separate process that runs in the background, and takes the payment details from the database, and processes the payment. If the payment is successful, it updates the database with the success status. If the payment fails, it updates the database with the failure status.&lt;/p>
&lt;p>The user can then check the status of their payment by making a GET request to /payment-status/{id}. We can have any number of statuses depending on the actual process, but the user will see essentially &amp;ldquo;Pending&amp;rdquo;, &amp;ldquo;Success&amp;rdquo; or &amp;ldquo;Failed&amp;rdquo;. If the user gets a &amp;ldquo;Pending&amp;rdquo; response, they can check back later to see if the payment was successful or not. If the user gets a &amp;ldquo;Failed&amp;rdquo; response, they can try again without having to worry about duplicate payments.&lt;/p>
&lt;p>On the back end we can have a scalable process that takes not-started payments from the database, and processes them. If the payment processor is down, or if there is a network issue, then the payment will just stay in the database until it can be processed. We can also have a retry mechanism in place to handle transient failures. If our whole system crashes, we can just restart it, and it will pick up where it left off without losing any data.&lt;/p>
&lt;p>The only additional complexity is that we now have a separate process that runs in the background to process the payments, and the front end needs to poll for the status of the payment. Neither of these are complicated or novel so not something any team should have any concerns implementing.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>Storage-First design is a powerful way to design systems that can handle any amount of process requests, and can recover from failure without losing any data. By making sure that your system is designed around storage, you can ensure that it can recover from failure, both ours and someone elses. Its easy to implement, and can be the difference between a system that can handle a few hundred users, and one that can handle millions. Its a simple change to the way we design our systems, but it can have a huge impact on the resilience and scalability of our systems.&lt;/p></description></item><item><title>Gamification to the Rescue</title><link>https://barneyparker.com/posts/gamification/</link><pubDate>Mon, 30 Mar 2026 16:30:00 +0000</pubDate><guid>https://barneyparker.com/posts/gamification/</guid><description>&lt;h1 id="what-is-gamification">What is Gamification?&lt;/h1>
&lt;p>Gamification is a method of improving by adding game-like elements to non-game contexts. It’s a way to make work more fun and engaging, and it can be a powerful tool for improving motivation and productivity.&lt;/p>
&lt;p>It sounds compeltely crazy, but it’s actually a really effective way to get people to do things they might not otherwise want to do. It’s all about tapping into our natural desire for competition, achievement, and recognition.&lt;/p>
&lt;h1 id="a-story-of-gamification">A Story of Gamification&lt;/h1>
&lt;p>A few years ago I was working at an organization that was cutting edge in its field. So much so that it had to invent much of the technology it used.&lt;/p>
&lt;p>If you know something can be done, its really easy to do it, and do it well. If you&amp;rsquo;re having to feel your way in the dark, then its hard to see the problems until they&amp;rsquo;re deeply embedded in the system. In our case we were so busy on the &amp;ldquo;can it even be done&amp;rdquo; that we weren&amp;rsquo;t looking at whether we were doing the basics well.&lt;/p>
&lt;p>One major area of concern was vulnerabiltiies. We had a lot of them, but we had such a backlog of features, tech debt and bugs, we just never seemed to find time to get to them. We had a lot of tools, but they were all just sitting there, not being used.&lt;/p>
&lt;p>We tried running &amp;ldquo;Vulnerability Sprints&amp;rdquo; - a week where we would focus on fixing vulnerabilities. It was a good idea, but it just didn&amp;rsquo;t work. We would get a few done, but then we would get distracted by other things, and the momentum would be lost.&lt;/p>
&lt;p>I was running a small SRE team trying to work on making our systems work better with each other. Vulnerabilities were a problem, but they weren&amp;rsquo;t my problem. I had my own backlog of work to do, and I just didn&amp;rsquo;t have the time to focus on them.&lt;/p>
&lt;p>Then we had a minor situation occur. It was nothing earth shattering, and nothing was at risk, but it was the first time something had gained the visibility that vulnerabilities were a problem we couldnt ignore. We had to do something that not only got on top of the problem, but something that lasted.&lt;/p>
&lt;p>My team didnt have much capacity, but I was asked to look into it. I wrote some scripts to pull some stats, and I was able to match them up to various tags on our cloud infrastructure. It was by no means perfect, but it was good enough to get a rough idea of where the vulnerabilities were coming from.&lt;/p>
&lt;p>I wrote a simple scheduled script that would pull the stats and push the data into our company reporting tools. I set up a dashboard that would show the number of vulnerabilities, and how they were trending over time. I also set up some alerts that would notify us when we had a certain number of vulnerabilities.&lt;/p>
&lt;p>And the tumbleweeds kept on tumbling. No one cared. We had the data, we had the tools, but no one was using them.&lt;/p>
&lt;p>Instead I madified the script to generate a set of simple HTML charts. I used that to create a set of (worst case first) leaderboards. Essentially the same data sliced and diced in different ways. I didn&amp;rsquo;t know what was going to work, so I made a few that seemed like they might be useful. I had the script send the report the the company-wide email distribution list.&lt;/p>
&lt;p>On day one we had:&lt;/p>
&lt;ul>
&lt;li>Critical: 6237&lt;/li>
&lt;li>High: 64598&lt;/li>
&lt;/ul>
&lt;p>The email went out at 3am, and by the time i logged in at 8:30 I had a few Slack messages from people asking about the report.&lt;/p>
&lt;p>By the end of the day I had more people talking, and some genuinely angry that I was attempting to make them look bad.&lt;/p>
&lt;p>My response to everyone was the same - &amp;ldquo;I&amp;rsquo;m not trying to make anyone look bad, i&amp;rsquo;m just making sure we all know where we stand&amp;rdquo;&lt;/p>
&lt;p>over the next few days I had engineers reaching out asking where i got the data from, what they could do to fix problems etc. It was a little overwhelming. By the end of the week i was regretting it, but the CTO reached out and couldn&amp;rsquo;t believe what a huge reaction there had been. Despite various attempts in the past, this was the biggest noise it had ever created.&lt;/p>
&lt;p>Among the various conversation my team identified a few key areas that were causing the most problems. The biggest being Container repositories. Teams created images, but never cleaned up old ones. The vulnerability scanners however were still scanning them, and they were still showing up in the reports. We had a lot of old images that were just sitting there, and they were causing a lot of noise. Engineers complained that they weren&amp;rsquo;t a risk so shouldnt be counted, but we had to draw the line somewhere so i pushed back.&lt;/p>
&lt;p>I did however keep telling people that I still wasn&amp;rsquo;t telling them to do anything they didnt want to do.&lt;/p>
&lt;p>I spent the first couple of days the next week writing a tool that scanned a repository, looked at all the places the images could be used, and then deleted images that weren&amp;rsquo;t needed any more. It could be run in dry-run mode, and it could be set on a schedule. By the time the third email went out, a few teams had implemented it in dry-run mode, and by the time the fourth email went out, a few teams had implemented it in production.&lt;/p>
&lt;p>Eventully we replaced my nieve script with a better tool that captured more data and was more accurate, but by the time I stopped sending my report emails we had:&lt;/p>
&lt;ul>
&lt;li>Critical: 298&lt;/li>
&lt;li>High: 12417&lt;/li>
&lt;/ul>
&lt;p>Those numbers weren&amp;rsquo;t perfect, but to put it into context we never told anyone to deal with vulnerabilities, we just published the top 10 worst offenders each week. We reduced cricital vulnerabilities to just 4.7% of the original Criticals, and 19.2% of the original Highs.&lt;/p>
&lt;h1 id="the-power-of-gamification">The Power of Gamification&lt;/h1>
&lt;p>The key was that we never told anyone to do anything, and we didnt ask anyone to de-prioritize work they were already planning.&lt;/p>
&lt;p>We, as a central team provided tools to do a lot of the heavy lifting, but we didnt force anyone to use them.&lt;/p>
&lt;p>We just made sure everyone knew where they stood, and let them decide what to do about it. We let them decide how to prioritize it, and we let them decide how to fix it.&lt;/p>
&lt;h1 id="after-the-game">After the Game&lt;/h1>
&lt;p>Once things were at a manageable level we introduced a new CI/CD module that blocked deployments if they have any critical vulnerabilities at all, and if they have more than 10 high vulnerabilities. We also helped teams set up Dependabot to keep their dependencies up to date. This stopped the flow of dependencies into production, but if we had done this on day one the whole company would have crumbled trying to deal with it. Instead we got on top of the problem, and then put in guard-rails to stop it from happening again.&lt;/p>
&lt;p>It may have taken time, but it was a sustainable solution that lasted. We didnt just fix the problem, we fixed the problem and then stopped it from happening again.&lt;/p></description></item><item><title>Laziness Engineering</title><link>https://barneyparker.com/posts/laziness-engineering/</link><pubDate>Mon, 30 Mar 2026 15:00:00 +0000</pubDate><guid>https://barneyparker.com/posts/laziness-engineering/</guid><description>&lt;h1 id="what-is-laziness-engineering">What is Laziness Engineering?&lt;/h1>
&lt;p>Lets face it - if you can avoid doing it, you would! Do what I hear you ask? well, anything really.&lt;/p>
&lt;p>More specifically I&amp;rsquo;m talking about building and running large-scale software systems. There are a lot of things that we do in software development that are just plain hard work, and if we can avoid doing them, then we should!&lt;/p>
&lt;p>The good news is that you dont have to work hard, but you do have to work hard to be lazy.&lt;/p>
&lt;h1 id="the-benefits-of-laziness-engineering">The Benefits of Laziness Engineering&lt;/h1>
&lt;p>There are lots of Laziness Engineering approaches, but we call them &amp;ldquo;productivity&amp;rdquo; tools and processes, because you dont get a pay rise for being lazy - but you should!&lt;/p>
&lt;p>The less you &lt;em>have&lt;/em> to do, the more you &lt;em>can&lt;/em> do. It all comes down to being better value for money as an engineer - after all, we&amp;rsquo;re not cheap resources.&lt;/p>
&lt;p>I love CI/CD pipelines. When I started out (before we even called it “engineering”), we passed zip files around on floppy disks. You had to remember to run tests, build, and upload everything by hand. Inevitably, someone forgot a step, and production broke. It was chaos.&lt;/p>
&lt;p>Someone invented Make, which meant I only had to figure it out once, put it in a Makefile, and then just run &lt;code>make build&lt;/code>. Suddenly, the build was one command—and I didn’t have to think about it.&lt;/p>
&lt;p>Makefiles are awesome, and definitely still have a big place, but things got more complicated (as they always do).&lt;/p>
&lt;p>Enter CI/CD pipelines: now you can hand off everything—linting, static analysis, SBOMs, docs, security scans, dependency updates, code formatting, coverage, test reporting, and more. All automated, all the time, on every commit or pull request.&lt;/p>
&lt;p>Something broken? Let the pipeline tell you. Need to keep dependencies fresh? Let Dependabot handle it, and let your pipeline do the rest.&lt;/p>
&lt;p>Of course, you still have to write good tests. If you don’t, the pipeline will happily deploy your broken code. But that’s still better than manually checking every build. And with the cloud, you can spin up ephemeral environments for every branch—test everything, auto-merge if it passes, and if not, the pipeline tells you exactly what failed.&lt;/p>
&lt;p>If something slips through and gets deployed, that’s on you. But now you know what broke—so write a test for it! Over time, your test suite gets stronger, catches more edge cases, and you can trust what you ship.&lt;/p>
&lt;p>CI/CD is great, but it can be a bit slow. That’s where Git hooks come in. Pre-commit hooks let you run tools before you even make a commit, catching dumb mistakes before they hit the pipeline.&lt;/p>
&lt;p>Take it to the extreme: if your tests and pipeline are solid, why wait for someone to approve your code? Merge it yourself! If it breaks, fix it. If it works, ship it faster.&lt;/p>
&lt;p>And now, with AI agents, you can automate even more!&lt;/p>
&lt;p>It sounds too good to be true, but if you repeat the process enough thats exactly where you end up.&lt;/p>
&lt;p>CI/CD is just one example, but the principle applies to everything. You can automate, delegate or avoid pretty much your whole job. Whats more, your team, your boss, your family, and you bank balance will love you for it!&lt;/p>
&lt;p>Be lazy and make the tools do the work for you. Focus on the things that really matter. Things like Coffee, podcasts, music (you could even play that guitar you have to look cool when on video calls&amp;hellip;.). You can even go outside and touch grass.&lt;/p></description></item><item><title>Telemetry, not Logs &amp; Metrics</title><link>https://barneyparker.com/posts/telemetry-not-log-and-metrics/</link><pubDate>Mon, 30 Mar 2026 10:00:00 +0000</pubDate><guid>https://barneyparker.com/posts/telemetry-not-log-and-metrics/</guid><description>&lt;h1 id="whats-wrong-with-logs-and-metrics">Whats wrong with Logs and Metrics?&lt;/h1>
&lt;p>For decades we&amp;rsquo;ve worked iwht logs and metrics.&lt;/p>
&lt;p>Our software developers have spewed out random pieces of text - some useful, some not so useful, and we&amp;rsquo;ve collected that information. They handily categorized it into levels, i.e. informational, warnings, errors etc.&lt;/p>
&lt;p>At the same time our infra has spewed out numbers that ean something. usually things like how much is the cpu doing, how much disk space do we have left. how many requests are we getting.&lt;/p>
&lt;p>Over time we got al ittle clever and started to include the magical Traces! These let us see a bit more detail into an operation, and how long function calls took.&lt;/p>
&lt;p>They&amp;rsquo;ve served us well. So well that we gave them a grand title: The Three Pillars.&lt;/p>
&lt;p>The thing is though, if we&amp;rsquo;re honest, they&amp;rsquo;re a bit shit really&amp;hellip;..&lt;/p>
&lt;p>If you know where to look, and what to look at, and you can pattern match across millions of log lines, and you squnit a little bit and tilt your head, sometimes you can see&amp;hellip;&amp;hellip; something&amp;hellip;.&lt;/p>
&lt;p>Metrics make some pretty charts though - and thats very satisfying&amp;hellip;.&lt;/p>
&lt;p>Except, they don&amp;rsquo;t tell you why something is happening. They dont tell you anything in themselves, they just show you that something is happening. its on you to decide what any of that means. In some ways its a little like Astrology.&lt;/p>
&lt;p>Not very 21st century tech&amp;hellip;..&lt;/p>
&lt;h1 id="whats-better">What&amp;rsquo;s better&lt;/h1>
&lt;p>Charity Majors has written, spoken and (correctly) ranted about observaility. And she has a point.&lt;/p>
&lt;p>What we really need is Telemetry.&lt;/p>
&lt;p>When I think about Telemetry i think about Formula-1 cars, Rockets.&lt;/p>
&lt;p>I dont remember the last time i saw people in the NASA control room grep&amp;rsquo;ing logs (although i&amp;rsquo;m sure it does happen!). The same with the track-side team at a Formula-1 race.&lt;/p>
&lt;p>Telemetry is about collecting information that lets us understand whats happening, even when we&amp;rsquo;re not there. It lets us understand the state of the system, and how it got there. It lets us understand why something is happening, not just that something is happening.&lt;/p>
&lt;p>The funny thing is, its actually not that hard to do, but it does require a slight change of mind-set.&lt;/p>
&lt;h1 id="the-properties-of-telemetry">The Properties of Telemetry&lt;/h1>
&lt;p>Telemetry becomes useful when its consistent, and when it has context. We call this High Cardinality.&lt;/p>
&lt;p>Logs have a problem in that a single system may be concurrently processing millions of requests, and if we want to understand the state of a single request we need to be able to correlate all the logs that relate to that request.&lt;/p>
&lt;p>Logs also have the problem that they only contain what the developer thought would be useful - and thats generally based upon their mental model of the system, which may not be accurate.&lt;/p>
&lt;p>Logs are also often essentially the debugging method of last resort where we output lots of &amp;ldquo;We got here&amp;rdquo; log messages in order to trace whats going on.&lt;/p>
&lt;p>Untangling all this can be a nightmare.&lt;/p>
&lt;p>Traces make this a bit better, but they still rely on the developer to have instrumented the code in the right way, and to have included the right information in the spans.&lt;/p>
&lt;h2 id="structured-logging">Structured Logging&lt;/h2>
&lt;p>The first step in untangling all of this is to structure our logs into a consistent, machine-readable format.&lt;/p>
&lt;p>There are a few formats we could use, but the most common is JSON. This is both human and machine readable.&lt;/p>
&lt;h2 id="canonical-logs">Canonical Logs&lt;/h2>
&lt;p>The second step is to output one single log line per &amp;ldquo;request&amp;rdquo;. Any job should create a single line of all the information, end-to-end for that job. This stops us having to pull otgether lots of different log lines to understand whats going on.&lt;/p>
&lt;h2 id="dimensionality">Dimensionality&lt;/h2>
&lt;p>The more fields we have in our structured, canonical logs, the more we can slice and dice our data to understand whats going on. Knowledge is power - and this is where the power of telemetry comes from.&lt;/p>
&lt;h2 id="cardinality">Cardinality&lt;/h2>
&lt;p>For each field of dimensionality, we want to have it as highly cardinal as possible. HTTP verbs are a simple example: they can be GET, POST, PUT, DELETE etc. If we just have a field called &amp;ldquo;method&amp;rdquo; with the value &amp;ldquo;GET&amp;rdquo;, then we can easily filter our logs to see all the GET requests. UserId is highly cardinal because there are potentially millions of users, and each user has a unique ID.&lt;/p>
&lt;h2 id="correlation-ids">Correlation Ids&lt;/h2>
&lt;p>Correlation Ids are a special type of field that we include in our logs to allow us to correlate logs across different systems. For example, if we have a request that goes through multiple microservices, we can include a correlation ID in each log line that relates to that request. This allows us to pull together all the logs that relate to that request, even if they are spread across multiple systems.&lt;/p>
&lt;h1 id="what-this-gives-us">What This Gives Us&lt;/h1>
&lt;p>This allows us to filter our logs to see all the requests from a particular user.&lt;/p>
&lt;p>The more cardinal our fields are, the more dimensionality we tend to start adding - one feeds into another. For example we start by including UserID, but a user is part of a specific customer, so we add CustomerID. A customer is part of a specific region, so we add Region. A region is part of a specific data centre, so we add DataCentre. Our customer is on a specific payment plan, so we include PlanID.And so on&amp;hellip;.&lt;/p>
&lt;p>Now we can query our logs to see all the requests from a particular user, or all the requests from a particular customer, or all the requests from a particular region, or all the requests from a particular payment plan. We can also start to correlate this with other information, such as metrics and traces, to get a more complete picture of whats going on.&lt;/p>
&lt;p>Dimensions can also be metrics - for example startTime, endTime, duration. Now we can query how may user in the US region took more than 1 second to compelte their request on the Enterprise plan.&lt;/p>
&lt;p>Try that with the old school pillars of logs and metrics!&lt;/p>
&lt;h1 id="isnt-this-open-telemetry">Isnt This Open Telemetry?&lt;/h1>
&lt;p>Yes and no. OpenTelemetry definitely advocates this approach, but really it just a transport mechanism for getting telemetry data from our systems to our observability platform. It doesn&amp;rsquo;t tell us how to structure our logs, or what fields to include, or how to correlate them. That&amp;rsquo;s up to us to figure out.&lt;/p>
&lt;p>As an example, a siple http request to an ecommerce system may show:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;span_name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;GET /products/8365&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;duration&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">836&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">404&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Product not found&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>On the other hand, we could also show:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;correlation_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;019d3e53-4f79-715e-b4e3-182a400092ca&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;method&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;GET&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;path&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/products/8365&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;timestamp&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2026-03-30T10:03:51.387Z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;duration&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">836&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;status&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">404&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Product not found&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;user&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;12345&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;customer_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;67890&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;region&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;us-east-1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;plan_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;enterprise&amp;#34;&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nt">&amp;#34;cart&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;item_count&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;total_value&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">123.45&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nt">&amp;#34;flags&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;black_friday&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;new_ui&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">false&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nt">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;code&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;PRODUCT_NOT_FOUND&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;message&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Product not found&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>The same request, but the second example gives us far more information. Did we need all that? maybe not this time, but if we had a problem with our product search, then having all that information would be invaluable in helping us understand what was going on, and how to fix it.&lt;/p>
&lt;h1 id="wont-this-create-a-lot-of-data">Wont this create a lot of data?&lt;/h1>
&lt;p>Why yes, yes it will!&lt;/p>
&lt;p>but thats the point. The more data we have, the more we can understand whats going on. The more we can understand whats going on, the better we can fix it when things go wrong, and the better we can improve it when things are working well.&lt;/p>
&lt;h1 id="tail-sampling">Tail Sampling&lt;/h1>
&lt;p>Tail sampling is a technique for reducing the amount of data we store. By making the storage decision &lt;em>after&lt;/em> the request compeltes, based on its outcome we an choose whther to keep it, how long to keep it, and where to keep it:&lt;/p>
&lt;ul>
&lt;li>Keep 100% of requests that result in an error&lt;/li>
&lt;li>Keep 75% of requests that take longer than 1 second&lt;/li>
&lt;li>Keep 25% of requests that take longer than 500ms&lt;/li>
&lt;li>Keep 5% of all other requests&lt;/li>
&lt;/ul>
&lt;p>Importantly, because we have a correlation ID, we can keep &lt;em>all&lt;/em> the telemetry when a failure occurs, not just the telemetry for one system. This lets us understand not only that a failure occured, but why it occurred and &lt;em>where&lt;/em> it occurred, even if that operation in itself wasnt a failure.&lt;/p>
&lt;h1 id="the-roi">The ROI&lt;/h1>
&lt;p>Implemented properly, the knowledge and understanding of a system that we can gain from telemetry is invaluable.&lt;/p>
&lt;p>We can use it for debugging, on-call, understanding features, user behaviour, cost analysis, and so much more. The ROI is huge, and the cost of implementing it is relatively low, especially when compared to the cost of not having it when things go wrong.&lt;/p>
&lt;p>Whats even better is we dont have to get it right on day one. We can start small, with just a few fields, and then iterate and improve over time.&lt;/p>
&lt;ul>
&lt;li>Each time there is a problem and there was data missing - add it so we capture it for next time&lt;/li>
&lt;li>When a product manager asks a question about user behaviour, add the fields to answer that question for next time&lt;/li>
&lt;/ul>
&lt;h1 id="summary">Summary&lt;/h1>
&lt;p>We&amp;rsquo;ve got a long way on the three pillars, but its time to move on. Telemetry is the future, and we should be embracing it. It gives us a much richer understanding of our systems, and allows us to fix problems faster, and improve our systems more effectively.&lt;/p>
&lt;p>Whats more it helps us get to be lazy. Information is power, and giving it to the people who need it means engineers are freed from trying to wrangle insights from logs and metrics, and can instead focus on building new features and improving the system.&lt;/p>
&lt;p>Every engineer should aspire to be lazy, and telemetry is a great way to achieve that.&lt;/p></description></item><item><title>Priorities in System Design</title><link>https://barneyparker.com/posts/prorities-in-system-design/</link><pubDate>Mon, 30 Mar 2026 09:00:00 +0000</pubDate><guid>https://barneyparker.com/posts/prorities-in-system-design/</guid><description>&lt;h1 id="introduction">Introduction&lt;/h1>
&lt;p>Is any system there are competing proprities that need to be balanced, however not all proirities are created equal!&lt;/p>
&lt;p>As an example, you may need to nesure you;re using Brand colors in your UI, but that is clearly less important than ensuring you;re not leaking credit card data.&lt;/p>
&lt;h1 id="the-priorities-of-_any_-system">The Priorities of &lt;em>any&lt;/em> System&lt;/h1>
&lt;p>Like the Asimov&amp;rsquo;s Three Laws of Robotics, there are certain priorities that should guide the design of any system. Each priority builds upon the previous one, but previous ones always take precedence over the next ones. In other words, you should never sacrifice a higher priority for the sake of a lower priority.&lt;/p>
&lt;p>They are also sub-grouped into three categories. Non-negotiable, Negotiable, and Evolutional:&lt;/p>
&lt;h2 id="non-negotiable-priorities">Non-Negotiable Priorities&lt;/h2>
&lt;h3 id="1-security">1. &lt;strong>Security&lt;/strong>&lt;/h3>
&lt;p>Beyond anything else, a system must be secure. We need to protect our users data, but we also need to ensure nobody can get root access to our servers. This is Priority One for the simple reason that the most secure system is the one that is turned off. Presumably thats not an option, so we need to work backwards from there and ensure we have the smallest possible attack surface, and that the surface we &lt;em>do&lt;/em> have is as well designed for security as we are able to make it.&lt;/p>
&lt;h3 id="2-durability">2. &lt;strong>Durability&lt;/strong>&lt;/h3>
&lt;p>Durability is an interesting word. Its often confused with Availability or Reliability, but its actually a different concept.&lt;/p>
&lt;p>Durability is about ensuring that data is not lost, even when something goes wrong. If we still have the data, at least we can try to do the thing again. If we lose it, it&amp;rsquo;s game over.&lt;/p>
&lt;p>This makes it an interesting prioity because it directly influences the design of the system. It affects the order of operations, the way we handle errors, and the way we design our data storage.&lt;/p>
&lt;h3 id="3-correctness">3. &lt;strong>Correctness&lt;/strong>&lt;/h3>
&lt;p>It seems obvious that a system needs to be Correct, but its surpising how often we seem to accept &amp;ldquo;bugs&amp;rdquo;. For some reason we seem to have accepted that software is inherently buggy, and that we just need to live with it. This is a failure of our design, not an inevitability of software development.&lt;/p>
&lt;p>Correctness is about ensuring that our system does what it is supposed to do, and that it does it correctly. Its obvious that we dont want our Bank to make a mistake and lose our money, but we also dont want our Social Media platform to accidentally post something we didnt intend to post. We need to ensure our customers receive the products they purchased and that they are not overcharged or undercharged. We need to ensure our users can trust our system to do what it is supposed to do, and to do it correctly.&lt;/p>
&lt;h2 id="negotiable-priorities">Negotiable Priorities&lt;/h2>
&lt;p>The following 3 Priorities are negotiable. You can&amp;rsquo;t sacrifice the non-negotiable priorities for the sake of these, but you can make trade-offs between them. Your system sits at a point somewhere in the triange between these three priorities, and you need to decide where that point is based on your users needs and your business goals.&lt;/p>
&lt;p>An obvious example is adding an extra 9 to your availability may reduce you latency and increase you costs. If you dont have the money, or if your users dont care about that extra 9, then you shouldnt add it. If you do have the money, and your users do care about that extra 9, then you should add it. But you shouldnt add it just because it sounds good, or because your boss wants it, or because your competitors have it. You should add it because it is the right thing to do for your users and your business.&lt;/p>
&lt;h3 id="4-availability">4. &lt;strong>Availability&lt;/strong>&lt;/h3>
&lt;p>At last, we get to Availability! This is often the first thing that gets thought about when designing a system - mostly because its a solved problem. Modern technology means we can achieve very high levels of availability with relative ease, and its common-place to over-spec a system to ensure it is always available.&lt;/p>
&lt;p>Using the SRE model though, we shouldn&amp;rsquo;t just throw money at the problem (more on this in the Cost section), we should actively design our system to &amp;ldquo;Just available enough&amp;rdquo;. We shouldn&amp;rsquo;t be unreasonable and aim for 100% availability just because it &lt;em>feels&lt;/em> right, or makes our boss feel warm and fuzzy inside. This is the first in a subsection of priorities&lt;/p>
&lt;h3 id="5-latency--performance">5. &lt;strong>Latency / Performance&lt;/strong>&lt;/h3>
&lt;p>I&amp;rsquo;ve given this Priority two names because they&amp;rsquo;re kind of interchangeable. Essentially we need answers, and we need them ~now!~ as soon as is pratically possible within the constraints of our system.&lt;/p>
&lt;p>Its technically a lower priority that availablity because i&amp;rsquo;d rather the process compeltes late than not even being able to start it in the first place.&lt;/p>
&lt;h2 id="6-cost">6. &lt;strong>Cost&lt;/strong>&lt;/h2>
&lt;p>Cost is the most easy to quanitify, and the most likely to be lied about! Companies often say thhey want the best rexperience for their users, and hey, look, it&amp;rsquo;s priority 6 - it barely matters, right?&lt;/p>
&lt;p>The reality is that cost is often the most important priority, we just dont like to talk about that. Really, this is the one that underlines all of the rest:&lt;/p>
&lt;blockquote>
&lt;p>We want the safest, most durable, most correct, most available, and best performing system that we can afford.&lt;/p>
&lt;/blockquote>
&lt;p>The interesting thing is, if 1-5 are done really well, costs tend to lower than if they are done poorly. It also tends to be the case that if 1-5 are done really well, users are more likely to pay for the system, which means we have more money to spend on the system, which means we can do 1-5 even better. Its a virtuous cycle.&lt;/p>
&lt;h2 id="evolutional-priorities">Evolutional Priorities&lt;/h2>
&lt;p>These are priorities that aren&amp;rsquo;t &lt;em>necessary&lt;/em> for the system to function, but they are important for the long-term success of the system. They are about ensuring the system can evolve over time, and that it can adapt to changing requirements and user needs.&lt;/p>
&lt;h3 id="7-self-sustaining">7. &lt;strong>Self-Sustaining&lt;/strong>&lt;/h3>
&lt;p>There are other evolutional priorities we could add here, but I think this one is the most important. A system should be designed to be self-sustaining, meaning it can operate with minimal human intervention. In the event of a problem, the system should be able to recover itself, or at least provide enough information for a human to intervene and fix the problem.&lt;/p>
&lt;p>This used to be extremely hard, but with modern infrastructure and automation tools, it&amp;rsquo;s becoming increasingly possible to design systems that are self-sustaining.&lt;/p>
&lt;p>Cloud systems emit events when all sorts of things happen (and we should emit custom events also). We can use those to trigger corrective actions. In the worst case we can alert a human who can A) fix, and then B) automate the fix for next time.&lt;/p>
&lt;h1 id="conclusion">Conclusion&lt;/h1>
&lt;p>When designing a system, it&amp;rsquo;s important to consider the priorities that should guide our design decisions.&lt;/p>
&lt;p>I&amp;rsquo;ve spent a lot of time on-call, and whether the problem is tiny or huge, waking up at 3am and trying to deal with a broken system isn&amp;rsquo;t fun. If we design our systems with these priorities in mind, we can reduce the likelihood of waking up at 3am, and we can make it easier to deal with the problems when they do happen.&lt;/p></description></item><item><title>Random Notes on System Design</title><link>https://barneyparker.com/posts/random-notes-on-system-design/</link><pubDate>Mon, 22 Dec 2025 11:05:00 +0000</pubDate><guid>https://barneyparker.com/posts/random-notes-on-system-design/</guid><description>&lt;h1 id="introduction">Introduction&lt;/h1>
&lt;p>Over the years I have picked up various insights and best practices related to system design. This post is a collection of random notes that I find useful when thinking about designing scalable and maintainable systems.&lt;/p>
&lt;p>They&amp;rsquo;re not particularly organized, however they come up time and again in my mind when considering different aspects of system design. Working with teams can sometimes be difficult becuase different people have different mental models of how systems should be designed. I hope that by sharing these notes, I can help align some of those mental models.&lt;/p>
&lt;h1 id="one-liners">One-Liners&lt;/h1>
&lt;ul>
&lt;li>Simple is generally the most resilient&lt;/li>
&lt;/ul>
&lt;h1 id="the-point-of-a-system">The point of a System&lt;/h1>
&lt;p>The point of &lt;em>any&lt;/em> system, whether it is digital or physical, is to fulfil a users need. A system is not a piece of art, it is not a monument to human ingenuity, it is not a testament to the skill of its creators. A system exists solely to serve its users.&lt;/p>
&lt;p>A system that does not serve its users is a failure, regardless of how well it is designed or how impressive its architecture may be.&lt;/p>
&lt;h1 id="evolution-as-an-agile-process">Evolution as an Agile process&lt;/h1>
&lt;p>Many companies say they are &amp;ldquo;Agile&amp;rdquo;, but the reality is they often are not. Using daily stand-ups, two-week sprints, and other Agile rituals does not make a company Agile. Agile coaches or Scrum Masters are not required, and often appear to sustain process over evolution. Somehow Agile got coopted into a set of rituals and ceremonies that have little to do with the original intent of Agile.&lt;/p>
&lt;p>Agile is the embodyment of continuous evolution. A system should be designed to evolve over time, to adapt to changing requirements, and to improve based on user feedback.&lt;/p>
&lt;p>In Darwins Theory of Evolution, the most important factor for survival is the ability to adapt. A System only needs to be &amp;ldquo;good enough&amp;rdquo; for its current environment, and to excel in that environment it needs to be &amp;ldquo;a little bit better than the competition&amp;rdquo;.&lt;/p>
&lt;p>By contrast I often see companies trying to design systems that are &amp;ldquo;perfect&amp;rdquo; from the start, or that are &amp;ldquo;future-proof&amp;rdquo;. This is a flawed approach, as it often leads to over-engineering, increased complexity, and a lack of adaptability. Essentially this is Waterfall thinking implemented using Agile rituals to tell ourselves we are Agile.&lt;/p>
&lt;p>A perfect process doesnt exist (after all, there is no perfect animal or plant in nature), however Agile gives us an evolutionary cycle:&lt;/p>
&lt;ul>
&lt;li>Build -&amp;gt; Measure -&amp;gt; Learn -&amp;gt; Adapt -&amp;gt; Repeat&lt;/li>
&lt;/ul>
&lt;p>Every iteration should end with a working system that we hope is better than the previous iteration. By learning from this system, we can adapt our design and improve it over time.&lt;/p>
&lt;p>Internally we should be using Metrics and Observability to measure how well our system is performing, and to identify areas for improvement. This data should inform our decisions about how to adapt our system in the next iteration.&lt;/p>
&lt;p>Externally we should be talking to users to gatehr feedback on how well our system is meeting their needs. This feedback should also inform our decisions about how to adapt our system in the next iteration.&lt;/p>
&lt;p>From this we can plan the next iteration. Sometimes that will be to add or extend functionality, but often it should be to wind back functionality because it was an evolutionary mis-step. Unfortunately thats often viewed as failure, when in reality its just part of the evolutionary process.&lt;/p>
&lt;p>To Quote Einstein:&lt;/p>
&lt;blockquote>
&lt;p>&amp;ldquo;A man who has never failed has never tried&amp;rdquo;&lt;/p>
&lt;/blockquote>
&lt;p>Failure should be more common that success in our evolutionary process, yet we often treat it as a taboo subject. We should embrace failure as a learning opportunity, and use it to inform our future decisions.&lt;/p>
&lt;h1 id="build-for-3am-when-its-all-gone-to-hell">Build for 3am, when it&amp;rsquo;s all gone to hell&amp;hellip;.&lt;/h1>
&lt;p>No matter what we do, something will always go wrong. Servers will crash, networks will go down, databases will become corrupted.&lt;/p>
&lt;p>When designing a system, we should always be thinking about how it will behave in the worst-case scenario.&lt;/p>
&lt;p>Computers dont care about data. People do.&lt;/p>
&lt;p>Many AWS services for example can have randomly generated names. For example if you create an S3 bucket but do not provide a name, the S3 service will generate one for you - and it&amp;rsquo;s not pretty. Thats fine because our IaC got that name and passed it to our Lambda functions and an env var, but what happens when we need to debug something at 3am because the system is down? We have no idea which bucket is which, and we have no idea which Lambda function is which. We are going to be in a world of pain. Its not impossible to debug, but its going to take a lot longer than if we had just used sensible names in the first place.&lt;/p>
&lt;p>When designing a system, we should always be thinking about how it will behave when things go wrong. We should be thinking about how we will debug it, how we will recover from failures, and how we will ensure that our users are not impacted.&lt;/p>
&lt;h1 id="incidents">Incidents&lt;/h1>
&lt;h2 id="analysis">Analysis&lt;/h2>
&lt;ul>
&lt;li>Triage is fast, but shallow. You&amp;rsquo;re analyzing to pin-point the problem so you can mitigate it quickly. You are not trying to find the root cause.&lt;/li>
&lt;li>Post-mortems are slow, but deep. You&amp;rsquo;re analyzing to find the root cause so you can prevent it from happening again. You are not trying to mitigate the problem quickly.&lt;/li>
&lt;/ul>
&lt;h2 id="log-files">Log Files&lt;/h2>
&lt;ul>
&lt;li>Log files are the API your operators use to debug an issue&lt;/li>
&lt;li>Log files were not intended for this specific issue&lt;/li>
&lt;li>OTel is making this better, but we still have a long way to go and they are often sampled to save money&lt;/li>
&lt;li>Design your log files with the assumption that they will be used to debug issues in the future&lt;/li>
&lt;li>Ensure your log files have sufficient context to understand what was happening at the time of the incident (Otel helps a lot here with tracing)&lt;/li>
&lt;/ul>
&lt;p>AI can be relly good at finding patterns and sequence of event in large, incoherent log files&lt;/p>
&lt;ul>
&lt;li>Consider using AI to help analyze log files during incidents&lt;/li>
&lt;li>AI might not be the smartest operator, but it doesnt get tired, and it doesn&amp;rsquo;t miss tiny changes between two nearly identical lines&lt;/li>
&lt;/ul></description></item><item><title>A Product Hierarchy of Need</title><link>https://barneyparker.com/posts/a-product-hierarchy-of-need/</link><pubDate>Tue, 19 Dec 2023 17:56:39 +0000</pubDate><guid>https://barneyparker.com/posts/a-product-hierarchy-of-need/</guid><description>&lt;h1 id="introduction">Introduction&lt;/h1>
&lt;p>Developing and running software systems is a complicated balancing act between keeping the lights on, satisfying various competing demands from the business and its customers, and staying ahead of the competition.  Attempting to do incorporate all of these needs while also working out the &amp;ldquo;right&amp;rdquo; thing to do next is near impossible. &lt;/p>
&lt;p>The SRE approach prioritises operational running of a system above all else, but even that is too general to help us.  Instead we need to define a language of work types, and agree how we will split our time based on a worst case situation.  As situations improve and we clear our backlogs of higher priority work, we can spread the remaining time in to higher-level work types.&lt;/p>
&lt;p>Classifying work with a hierarchical &amp;ldquo;type&amp;rdquo; and defining the priority of each work type can help us decide where to focus our energy, and also to help justify why some work was chosen over others.&lt;/p>
&lt;h1 id="tldr">TL;DR&lt;/h1>
&lt;p>If we classify work in our backlogs according to its &amp;ldquo;type&amp;rdquo;, and we prioritise work types based on importance to running stable, reliable, compliant systems we can free ourselves to do &amp;ldquo;higher level&amp;rdquo; work without concern for the behaviour of our products because our foundations will be solid enough to support us without the need for constant attention.&lt;/p>
&lt;p>While we draw the hierarchy of important as a triangle, with foundational work being the largest and lowest part of the triangle, we intend to &amp;ldquo;invert&amp;rdquo; the triangle by ensuring the foundations are so well taken care of that we spend the least time.  We can only get there by consistently prioritising the foundations beyond all other work.&lt;/p>
&lt;h1 id="human-hierarchy-of-needs">Human Hierarchy of Needs&lt;/h1>
&lt;p>Maslow&amp;rsquo;s hierarchy of needs is a theory from psychology that gives a five-tier model of human needs, depicted as a pyramid.  The segments location in the pyramid shows priority; lower levels have a higher priority overriding lower priority needs at higher layer.&lt;/p>
&lt;p>&lt;img src="https://barneyparker.com/posts/a-product-hierarchy-of-need/https://www.simplypsychology.org/wp-content/uploads/maslow-hierachy-of-needs-min-1024x724.jpg" alt="Maslow&amp;rsquo;s hierarchy of needs is a model for understanding the motivations for human behavior. It maps different motivations onto a pyramid, with each level representing a different human need. These include physiological needs, safety, love and belonging, esteem, and self-actualization." title="Maslows Hierarchy of Need">&lt;/p>
&lt;p>The idea is that base needs such as food and water are more important than higher level needs such as education (self-improvement).  If you have no food or water a person is unlikely to be concerned with whether they are going to pass an exam.&lt;/p>
&lt;h1 id="product-hierarchy-of-needs">Product Hierarchy of needs&lt;/h1>
&lt;p>Technical products are very similar to a person.  While they don&amp;rsquo;t need food and water, they do need basic working hardware to run on.  If a system isn&amp;rsquo;t even able to stay online, its owners are unlikely to care about the outcome from the latest spike end engineer has spent investigating a potential feature.&lt;/p>
&lt;p>While the hierarchy of human needs is relatively simple to understand, its not immediately clear what the hierarchy should be for a product, yet its not particularly hard to find an obvious home for the majority of things product teams regularly work on.&lt;/p>
&lt;h2 id="httpsstaticthenounprojectcompng1876478-200pngproduct-self-actualisation">&lt;img src="https://barneyparker.com/posts/a-product-hierarchy-of-need/https://static.thenounproject.com/png/1876478-200.png" alt="">Product Self-Actualisation&lt;/h2>
&lt;p>For a human, self-actualisation refers to becoming better than ones current self.  For a product this translates fairly simply in to discovery work or in Agile terms, a Spike.&lt;/p>
&lt;p>A Spike is used to try out some ideas.  They don&amp;rsquo;t make it to production in themselves but they do help us learn how an idea might work, what the constraints are, think about the things we might have otherwise not have fully understood.  In this sense a Spike is the betterment of the product and should sit at the top level.  If there are other problems with our product we don&amp;rsquo;t want to spend time on blue-sky thinking when customers are leaving and not paying our bills.&lt;/p>
&lt;h2 id="httpsstaticthenounprojectcompng4737061-200pngproduct-esteem">&lt;img src="https://barneyparker.com/posts/a-product-hierarchy-of-need/https://static.thenounproject.com/png/4737061-200.png" alt="">Product Esteem&lt;/h2>
&lt;p>Esteem for a human is where, in the developed world at least, we spend a lot of our time.  Doing our jobs to make our products better, implementing new features that we know our customers want.  &lt;/p>
&lt;p>It is enjoyable to add new features, or to improve old ones, but again its not particularly important if our service wont stay online.  We can only afford to do this if our lower level needs are already fulfilled and we have spare capacity to do this work.&lt;/p>
&lt;h2 id="httpsstaticthenounprojectcompng1422688-200pngproduct-love-and-belonging">&lt;img src="https://barneyparker.com/posts/a-product-hierarchy-of-need/https://static.thenounproject.com/png/1422688-200.png" alt="">Product Love and Belonging&lt;/h2>
&lt;p>Our products don&amp;rsquo;t work in a vacuum.  They have customers (whether that it a human or another system doesn&amp;rsquo;t matter) who we need to love our product.  They need to be able to rely on our product to work as expected.&lt;/p>
&lt;p>A prime candidate for this category is bug fixing.  The product isn&amp;rsquo;t &amp;ldquo;broken&amp;rdquo; as such, but it may have unexpected behaviour in certain circumstances, or may perform poorly.  These are the thing that drive customers away towards our competitors because they get irritated by the problems the product causes them.&lt;/p>
&lt;p>Fixing bugs is more important than adding new features because we need to keep current customers happy, but as with the other higher categories, its not as important as a system that can&amp;rsquo;t even stay up to serve our customers in the first place.&lt;/p>
&lt;h2 id="httpsstaticthenounprojectcompng5610096-200pngproduct-safety-needs">&lt;img src="https://barneyparker.com/posts/a-product-hierarchy-of-need/https://static.thenounproject.com/png/5610096-200.png" alt="">Product Safety Needs&lt;/h2>
&lt;p>There are some things related to a product that draw a line between prototype systems that we wouldn&amp;rsquo;t trust, and professional systems that we would.&lt;/p>
&lt;p>Product Safety covers things such as data security, authentication and authorisation.  We need to be sure that when a user accesses our product they only see data to which they have been given access.  We also need to be sure that only the authorised users are allowed access.&lt;/p>
&lt;p>While bugs are annoying, its even worse to find out someone else has access to our banking information for example.  Personally Identifiable Information has legal requirements to ensure it is handled properly.  If we aren&amp;rsquo;t sure this is being handled well its almost guaranteed that all higher-level work will be stopped until this is resolved.&lt;/p>
&lt;h2 id="httpsstaticthenounprojectcompng1888525-200pngproduct-physiological-needs">&lt;img src="https://barneyparker.com/posts/a-product-hierarchy-of-need/https://static.thenounproject.com/png/1888525-200.png" alt="">Product Physiological Needs &lt;/h2>
&lt;p>Products, like people have physiological requirements.  They need to run on infrastructure, and that infrastructure needs to be available for customers to access it.&lt;/p>
&lt;p>At this layer we need to include not just the infrastructure itself, but connectivity between components, the ability to detect failures and the ability to resolve problems before a customer notices.  Here we include on-call rotations, run-books, monitoring, alerting, dashboards etc.&lt;/p>
&lt;p>This is an area we would, in an ideal world not like to think about.  a huge reason why we use AWS is because it provides an awful lot out-of-the-box to help us with this layer.&lt;/p>
&lt;h1 id="how-does-this-relate-to-tech-products">How does this relate to Tech Products?&lt;/h1>
&lt;p>Agile prescribes working in short sprints, with each sprint delivering a working product.&lt;/p>
&lt;p>What Agile doesn&amp;rsquo;t do however is tell us how to decide what to work on.&lt;/p>
&lt;p>Its common to spend time defining work, refining the work to manageable chunks, and estimating the complexity to help us break things down in to chunks we can deliver in a single sprint.  We then load up a sprint with enough complexity that we can deliver what we said we would.&lt;/p>
&lt;p>In principal that&amp;rsquo;s fine, except for one small problem - who decide how important a chunk of work is?&lt;/p>
&lt;p>The hierarchy of human needs, when drawn as the pyramid above shows priority in therms of vertical location, and importance as width.  What it doesn&amp;rsquo;t show however is how much time is spent on each section.&lt;/p>
&lt;p>A caveman might spend most of their time hunting for food and water.  They will also spend a reasonable amount of time on ensuring their own personal safety.  They want to spend some time with friends and family, ensuring their place in their community.  They spend a little time on providing for their community in general to maintain their place in society and if there is any time left over, they might practice their skills to become better.&lt;/p>
&lt;p>In the developed world however, the pyramid is turned on its head.  I spend very little time on food and water - i turn on a tap and i can drink, and I open the fridge to get food.  I own a home with locks on them although I do spend a little time occasionally making sure i repair things, and i mow my garden.  I have a family which i spend my spare time with as much as i can.  I work every day in a job I enjoy and get paid for doing.  It takes up a lot of my time, but i get respect for doing my job.  I spend a a good deal of time on reading about systems engineering, AWS services, and gaining certifications etc.  Suddenly the base layer things are small, working towards more and more time spent at higher layers.&lt;/p>
&lt;p>The hierarchy of needs of a product are identical in many respects.  If we have tickets relating to the lowest layer we should prioritise those over all else.  The fundamental foundations of our product are the most important thing to work on, because without them everything else crumbles.&lt;/p>
&lt;p>If we have less work at the bottom layer to fill our sprint, we can select tasks from the next layer up.  As we work harder and harder to remove work at the bottom of the pyramid, we need to spend less and less time on it and can afford to spend our available time on progressively higher layers.  Over time, lower layers become less cumbersome, and we can spend more and more of our time working on features, and investigating ideas on how the future might look.&lt;/p>
&lt;p>When a problem occurs at a lower layer however, the hierarchy steps in and says it &lt;em>must&lt;/em>  be dealt with before anything else.  We then maintain a solid, healthy, reliable system that our users are happy with, and continue paying for.&lt;/p>
&lt;h1 id="hierarchy-of-needs-and-incident-management">Hierarchy of Needs and Incident Management&lt;/h1>
&lt;p>Given the hierarchy of needs, it becomes relatively clear how we should infer &amp;ldquo;importance&amp;rdquo; of a type of work.  This extends further in to the territory of incident management!&lt;/p>
&lt;p>Incidents are measured as P1/2/3.  P1 is necessarily the most important, and potentially damaging to the product.  It is therefore reasonable to say that any incident in the Physiology layer should be a P1.  Similarly incidents in the Safety layer aren&amp;rsquo;t normally quite as damaging - incorrect security configuration or a DB authentication problem aren&amp;rsquo;t a complete outage, but they definitely deserve to be a P2.&lt;/p>
&lt;p>Bugs however are pretty annoying.  They annoy users, and they might cause some support work to occur.  we probably don&amp;rsquo;t need to get anyone out of bed at 3am though.  In that sense we can say P3 incidents live within the Love/Belonging layer.&lt;/p>
&lt;p>Beyond P1/2/3 we don&amp;rsquo;t alert, but given that this is now squarely in to product development and future investigations, that makes sense.&lt;/p>
&lt;p>Any work that is needed in the lower three levels has the potential to become an incident if we don&amp;rsquo;t deal with it.  &lt;/p>
&lt;h1 id="hon-and-okrs">HoN and OKRs&lt;/h1>
&lt;p>In [X Company] we use OKRs to steer our products over the medium to long term.  OKRs tend to focus on the future - where we &lt;em>want&lt;/em>  to be in 3 or 6 months.&lt;/p>
&lt;p>OKRs contain two specific types of work - work we have thought about, refined, and planned far enough to put them in to a fairly specific OKR.  This is where a squad gets to show off to the rest of the business and to its customers.  This is the Esteem layer.&lt;/p>
&lt;p>Beyond the work we have already planned there is work that the business or our customers have asked for but we can;t necessarily do just yet.  We need a spike to evaluate it or to define it more clearly.  As we decided earlier, this sits in the Self-Actualisation layer.&lt;/p>
&lt;h1 id="we-cant-stop-developing-features-our-customers-wont-be-happy">We can&amp;rsquo;t stop developing features, our customers wont be happy!&lt;/h1>
&lt;p>That is partially true.  Its is important to ensure our product is seen to be improving, but stability and reliability is an improvement in itself.  There is a good argument that reliability is actually the only feature that all products have, and is therefore the only feature that truly matters.&lt;/p>
&lt;p>&lt;strong>Self Actualisation:&lt;/strong> 0%&lt;/p>
&lt;p>&lt;strong>Esteem:&lt;/strong> 5%&lt;/p>
&lt;p>&lt;strong>Belonging:&lt;/strong> 16.5%&lt;/p>
&lt;p>&lt;strong>Safety:&lt;/strong> 33%&lt;/p>
&lt;p>&lt;strong>Physiological:&lt;/strong> 45%&lt;/p>
&lt;p>This might seem a little extreme, but if there are 45% of complexity tasks available at the lowest level, it should be clear that you really should be prioritising those and not being too concerned about future product development work.  You might still need to do &lt;em>some&lt;/em>  feature work, but its decidedly less important than fixing bugs or ensuring data integrity.&lt;/p>
&lt;p>Over time, assuming you&amp;rsquo;re doing the right work, the number of available tasks at the lower levels should decrease.  this frees up more time further up the stack allowing you to spend additional time on more interesting work.  Eventually the stack turns upside down, and almost no time will be spent at the base layer, and almost all of your time can be spent on features and really thinking about the future.&lt;/p>
&lt;h1 id="closing-thoughts">Closing Thoughts&lt;/h1>
&lt;p>Sprints can be organised in many different ways, but consistently prioritising the more fundamental requirements of a product will set things up for success and free up more time for the interesting work we all came here to do.  Ignoring it will lead to problems, take time away from delivering what customers want, and will eventually lead to failure of the product&lt;/p>
&lt;p>This is true even for a potentially great product.&lt;/p>
&lt;h1 id="appendix-a---specific-work-categorisation">Appendix A - specific work categorisation&lt;/h1>
&lt;p>All work should exist here.  If a type of work doesn&amp;rsquo;t appear to be listed, please leave a comment to allow discussion on best placement&lt;/p>
&lt;h2 id="product-self-actualisation">Product Self-Actualisation&lt;/h2>
&lt;ul>
&lt;li>Spikes&lt;/li>
&lt;/ul>
&lt;h2 id="product-esteem">Product Esteem&lt;/h2>
&lt;ul>
&lt;li>OKR specified Work&lt;/li>
&lt;/ul>
&lt;h2 id="product-love-and-belonging">Product Love and Belonging&lt;/h2>
&lt;ul>
&lt;li>Bug fixes&lt;/li>
&lt;/ul>
&lt;h2 id="product-safety-needs">Product Safety Needs&lt;/h2>
&lt;ul>
&lt;li>Alerting&lt;/li>
&lt;li>Dashboards&lt;/li>
&lt;li>Vulnerability Management&lt;/li>
&lt;li>Cost Awareness&lt;/li>
&lt;/ul>
&lt;h2 id="product-physiology-needs">Product Physiology Needs&lt;/h2>
&lt;ul>
&lt;li>CI/CD&lt;/li>
&lt;li>Metrics&lt;/li>
&lt;li>No reliance on other environments&lt;/li>
&lt;li>Leverage Managed Services&lt;/li>
&lt;/ul></description></item><item><title>React Build-less MVP</title><link>https://barneyparker.com/posts/react-build-less-mvp/</link><pubDate>Tue, 12 Dec 2023 09:48:43 +0000</pubDate><guid>https://barneyparker.com/posts/react-build-less-mvp/</guid><description>&lt;h1 id="going-build-less">Going Build-less&lt;/h1>
&lt;p>I don&amp;rsquo;t do a huge amount of front end work, but from time to time I do need to put together a small front-end.&lt;/p>
&lt;p>Front-end work is some of the most complicated tech work there is. Not only do you need to know HTML, CSS and JavaScript, but you need to know how to configure and use build tools, npm, frameworks, and handle a bunch of security things such as CORS, CSP, and a whole bunch of other odds and ends!&lt;/p>
&lt;p>As you might expect, unless you;re doing this stuff daily, its hard to keep it all in your head, so removing any piece makes life much easier.&lt;/p>
&lt;p>One area I have found particularly useful is being able to use a framework such as React, but without needing a build step. Doing this is easier than it might appear, and only takes one or two little tricks to make it work.&lt;/p>
&lt;h1 id="removing-jsx">Removing JSX&lt;/h1>
&lt;p>The first piece of the puzzle is removing JSX. JSX looks like HTML, but actually gets compiled to a bunch of calls to Reacts &lt;code>createElement&lt;/code> function. In turn this builds out the Virtual-DOM, and React then does the diff/update steps etc. Without a build step we need some way to do that in the browser.&lt;/p>
&lt;p>&lt;a href="https://www.npmjs.com/package/htm">Htm&lt;/a> is a small library by Jason Miller that takes tagged template literals that look pretty close to JSX, and turns it in to the &lt;code>createElement&lt;/code> structure used by React!&lt;/p>
&lt;p>There is one minor different in that you need to refer to a component as &lt;code>&amp;lt;${myComponent}&amp;gt;&lt;/code> instead of plain &lt;code>&amp;lt;MyComponent&amp;gt;&lt;/code>, but otherwise everything behaves the same.&lt;/p>
&lt;p>H﻿tm needs to be bound to createElement to make it work so its good to do that in its own little module and export the html function in a file called &lt;code>html.js&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="kr">import&lt;/span> &lt;span class="nx">htm&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;https://esm.sh/htm@3.1.1&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">createElement&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;react&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">html&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">htm&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">bind&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">createElement&lt;/span>&lt;span class="p">);&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="import-maps">Import Maps&lt;/h1>
&lt;p>Normally without a build you would have to import React like this:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">useState&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;https://esm.sh/react@18.2.0&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>This is a bit clunky and cumbersome, so instead we can leverage import maps to make that look a bit nicer.&lt;/p>
&lt;p>In the &lt;code>head&lt;/code> section of your html document include the following:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="nx">script&lt;/span> &lt;span class="nx">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;importmap&amp;#34;&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;imports&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;react&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span>&lt;span class="s2">&amp;#34;https://esm.sh/react@18.2.0&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="err">/script&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>In your JS files you can now do:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">useState&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;react&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="a-simple-app-example">A Simple App Example&lt;/h1>
&lt;p>To test all this out we need a simple hello-world example. Create a file called App.js containing the following:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="c1">// @ts-ignore
&lt;/span>&lt;span class="c1">&lt;/span>&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">html&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;html&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">useState&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;react&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">Counter&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="nx">count&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">setCount&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">useState&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">0&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="nx">html&lt;/span>&lt;span class="sb">`
&lt;/span>&lt;span class="sb"> &amp;lt;button onClick=&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">setCount&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">count&lt;/span> &lt;span class="o">+&lt;/span> &lt;span class="mi">1&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">&amp;gt;Click Me [&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">count&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">]&amp;lt;/button&amp;gt;
&lt;/span>&lt;span class="sb"> `&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="p">};&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">App&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">()&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">html&lt;/span>&lt;span class="sb">`
&lt;/span>&lt;span class="sb"> &amp;lt;div&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;h1&amp;gt;Hello (Build-less) World!&amp;lt;/h1&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">Counter&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> /&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;/div&amp;gt;
&lt;/span>&lt;span class="sb">`&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>This contains two components, the &lt;code>App&lt;/code> component which we will call to start our app, and a local &lt;code>Counter&lt;/code> component. You can see on line 18 that we needed to reference the &lt;code>Counter&lt;/code> component as a variable rather than an element name, but otherwise it looks exactly like a normal React component.&lt;/p>
&lt;h1 id="tying-it-all-together">Tying it all together&lt;/h1>
&lt;p>As normal, we bring this all together with an index.html document which contains the import map, the root div that will contain the application, and a small inline bootstrap script to kick it all off:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="cp">&amp;lt;!DOCTYPE html&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">html&lt;/span> &lt;span class="na">lang&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;en&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">charset&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;UTF-8&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">meta&lt;/span> &lt;span class="na">name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;viewport&amp;#34;&lt;/span> &lt;span class="na">content&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;width=device-width, initial-scale=1.0&amp;#34;&lt;/span> &lt;span class="p">/&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;importmap&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;imports&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;react&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;https://esm.sh/react@18.2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;html&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;/html.js&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>Build-less React MVP&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">title&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">head&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">div&lt;/span> &lt;span class="na">id&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;root&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>This will be replaced&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">div&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;module&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">html&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;html&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">createRoot&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;https://esm.sh/react-dom@18.2.0&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">App&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;./App.js&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;span class="kr">const&lt;/span> &lt;span class="nx">root&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">createRoot&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nb">document&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">getElementById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;root&amp;#39;&lt;/span>&lt;span class="p">));&lt;/span>
&lt;span class="nx">root&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">render&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">html&lt;/span>&lt;span class="sb">`&amp;lt;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">App&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> /&amp;gt;`&lt;/span>&lt;span class="p">);&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">body&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">html&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>N﻿ote:&lt;/strong> we import react-dom with a full URL since this is the only place we&amp;rsquo;ll use it, so it isnt necessary to add it to the import map.&lt;/p>
&lt;p>A﻿s we go though app development, we can add other external scripts to the import map. for example we can import react-router-dom by adding:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;imports&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="p">...&lt;/span>
&lt;span class="s2">&amp;#34;react-reouter-dom&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;https://esm.sh/react-router-dom@6.4.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">...&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>a﻿nd then :something like&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">html&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;html&amp;#39;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">BrowserRouter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Routes&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">Route&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;react-router-dom&amp;#39;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">MyComponent&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;/Components/MyComponent&amp;#39;&lt;/span>
&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">NotFound&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;/Components/NotFound&amp;#39;&lt;/span>
&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">App&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">{}&lt;/span> &lt;span class="nx">html&lt;/span>&lt;span class="sb">`
&lt;/span>&lt;span class="sb"> &amp;lt;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">BrowserRouter&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">Routes&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb">&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">Route&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> path=&amp;#34;/&amp;#34; element=&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">html&lt;/span>&lt;span class="sb">`&amp;lt;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">MyComponent&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> /&amp;gt;`&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> /&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">Route&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> path=&amp;#34;*&amp;#34; element=&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">html&lt;/span>&lt;span class="sb">`&amp;lt;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nx">NotFound&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> /&amp;gt;`&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="sb"> /&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;//&amp;gt;
&lt;/span>&lt;span class="sb"> &amp;lt;//&amp;gt;
&lt;/span>&lt;span class="sb">`&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="running-locally">Running Locally&lt;/h1>
&lt;p>As a final touch, all this can be run locally using something like the http-server utility from npm. You&amp;rsquo;ll need to run it in SSL mode which requires an SSL certificate which can be created with:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-shell" data-lang="shell">openssl req -newkey rsa:2048 -new -nodes -x509 -days &lt;span class="m">3650&lt;/span> -keyout key.pem -out cert.pem
&lt;/code>&lt;/pre>&lt;/div>&lt;p>and then start the server with the command:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-shell" data-lang="shell">http-server -S -C cert.pem
&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Import Maps to simplify Browser Based ESM</title><link>https://barneyparker.com/posts/import-maps-to-simplify-browser-based-esm/</link><pubDate>Tue, 12 Dec 2023 08:34:47 +0000</pubDate><guid>https://barneyparker.com/posts/import-maps-to-simplify-browser-based-esm/</guid><description>&lt;p>I﻿mport maps let you use short names when importing packages in the browser rather than using &lt;code>script&lt;/code> tags.&lt;/p>
&lt;p>I﻿n order to mark a script as an import map, the &lt;code>script&lt;/code> tag needs a &lt;code>type&lt;/code> attribute with a value of &lt;code>importmap&lt;/code>.&lt;/p>
&lt;p>T﻿he contents should be an object with a single attribute of &lt;code>imports&lt;/code> which is an object where the keys are the name you will use to refer to a package, and the value is the URL to the package. e.g:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-html" data-lang="html">&lt;span class="p">&amp;lt;&lt;/span>&lt;span class="nt">script&lt;/span> &lt;span class="na">type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">&amp;#34;importmap&amp;#34;&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;imports&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s2">&amp;#34;React&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;https://esm.sh/react@18.2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;react-dom&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;https://esm.sh/react-dom@18.2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;htm&amp;#34;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s2">&amp;#34;https://esm.sh/htm@3.1.1&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">&amp;lt;/&lt;/span>&lt;span class="nt">script&lt;/span>&lt;span class="p">&amp;gt;&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>T﻿his should be placed in the &lt;code>head&lt;/code> section. The parser reads the import map, but doesn&amp;rsquo;t preload the imports. instead it waits until a package gets imported, and then translates it to its &lt;em>real&lt;/em> url for loading:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="kr">import&lt;/span> &lt;span class="nx">htm&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;htm&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/code>&lt;/pre>&lt;/div></description></item><item><title>Uploading File Trees to S3 with Terraform</title><link>https://barneyparker.com/posts/uploading-file-trees-to-s3-with-terraform/</link><pubDate>Fri, 02 Jul 2021 14:50:30 +0000</pubDate><guid>https://barneyparker.com/posts/uploading-file-trees-to-s3-with-terraform/</guid><description>&lt;p>Uploading a single file to S3 using Terraform is pretty simple, but sometimes you need to upload a whole folder. If you&amp;rsquo;re serving the files using S3 as a website, or through CloudFront you also need to make sure you set the correct mime types and eTags. Its actually a whole lot simpler than you might think!&lt;/p>
&lt;h2 id="single-file-uploads">Single File Uploads&lt;/h2>
&lt;p>A single file is easy enough to upload:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_object&amp;#34; &amp;#34;single_file&amp;#34;&lt;/span> {
&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_s3_bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">web&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;span class="n"> key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;index.hmtl&amp;#34;&lt;/span>
&lt;span class="n"> source&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.module}/content/index.html&amp;#34;&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>While that will upload a file, it will get a mime type of &lt;code>application/octet-stream&lt;/code> which means if you&amp;rsquo;re trying to serve it through CloudFront it will download rather than loading as the right document type. To set the right mime type, all thats needed is:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_object&amp;#34; &amp;#34;content&amp;#34;&lt;/span> {
&lt;span class="p">...&lt;/span>
&lt;span class="n"> content_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;text/html&amp;#34;&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Often a browser will perform a &lt;code>HEAD&lt;/code> request to see if a file has changed since the last time it downloaded it. The easiest way to check is via the &lt;code>etag&lt;/code> header which contains a hash of the contents. We can also add that automatically:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_object&amp;#34; &amp;#34;content&amp;#34;&lt;/span> {
&lt;span class="p">...&lt;/span>
&lt;span class="n"> etag&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">filemd5&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/content/index.html&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="extending-to-multiple-files">Extending to Multiple Files&lt;/h2>
&lt;p>Modern versions of terraform let is create multiple resources using the &lt;code>for_each&lt;/code> iteration operator. To get a list of the files we want to upload we can use the &lt;code>fileset&lt;/code> function which collects a list of files at a path based on a pattern.&lt;/p>
&lt;p>&lt;code>fileset&lt;/code> takes two parameters, a root path and a pattern to match files against. The pattern field is able to allow for recursively searching a folder tree using the &lt;code>**&lt;/code> globbing operator, so we could for example do something like:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_object&amp;#34; &amp;#34;content&amp;#34;&lt;/span> {
&lt;span class="n"> for_each&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">fileset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;../web/build&amp;#34;, &amp;#34;**/*.hmtl&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_s3_bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">web&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;span class="n"> key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">each&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">key&lt;/span>
&lt;span class="n"> source&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.module}/content/${each.key}&amp;#34;&lt;/span>
&lt;span class="n"> content_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;text/html&amp;#34;&lt;/span>
&lt;span class="n"> etag&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">filemd5&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/content/${each.key}&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Which is a pretty simple way of finding all the files ending in &lt;code>.html&lt;/code> and adding the to the bucket with the right mime type. Its a bit cumbersome working this way though because now we need to add a new block for JS, another for CSS, one for JPGs and so on&amp;hellip;..&lt;/p>
&lt;h2 id="mime-type-lookups">Mime-Type Lookups&lt;/h2>
&lt;p>Using some clever indexing we can save ourselves an awful lot of effort by creating a map of file extensions and the mime type they map to. Inside the &lt;code>for_each&lt;/code> loop we can dissect the filename to get the file extension, and then look up what mime type it maps to:&lt;/p>
&lt;p>Our map of mime types should look like:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">locals&lt;/span> {
&lt;span class="n"> mime_types&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;span class="n"> &amp;#34;css&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;text/css&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;html&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;text/html&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;ico&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;image/vnd.microsoft.icon&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;js&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;application/javascript&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;json&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;application/json&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;map&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;application/json&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;png&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;image/png&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;svg&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;image/svg+xml&amp;#34;&lt;/span>
&lt;span class="n"> &amp;#34;txt&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;text/plain&amp;#34;&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>we need to cover every file type we have, and Terraform will fail to plan if a file extension is added that we don&amp;rsquo;t have a type for, but at least we don&amp;rsquo;t end up with the wrong mime type!&lt;/p>
&lt;p>To do the lookup we need to &lt;code>split&lt;/code> the file name and path by the &lt;code>.&lt;/code> character, and then get the last element and use that in a &lt;code>lookup&lt;/code> against the map. Using a local actually gives us an &lt;code>object&lt;/code> rather than a &lt;code>map&lt;/code>, but you can convert using the &lt;code>toset&lt;/code> function.&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_s3_bucket_object&amp;#34; &amp;#34;content&amp;#34;&lt;/span> {
&lt;span class="n"> for_each&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">fileset&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/content&amp;#34;, &amp;#34;**/*.*&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> bucket&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_s3_bucket&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">web&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;span class="n"> key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">each&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">key&lt;/span>
&lt;span class="n"> source&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.module}/content/${each.key}&amp;#34;&lt;/span>
&lt;span class="n"> content_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">lookup&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">tomap&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">local&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">mime_types&lt;/span>&lt;span class="p">),&lt;/span> &lt;span class="k">element&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">split&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;.&amp;#34;, each.key), length(split(&amp;#34;.&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">each&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">key&lt;/span>&lt;span class="p">))&lt;/span> &lt;span class="err">-&lt;/span> &lt;span class="m">1&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="n"> etag&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">filemd5&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/content/${each.key}&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now every file will be found, mapped to a valid mime-type, an &lt;code>etag&lt;/code> calculated and then be individually uploaded to S3! Since the &lt;code>etag&lt;/code> is set, subsequent runs will only upload files that have changed!&lt;/p></description></item><item><title>Authenticate Users with Azure OIDC on AWS Application Load Balancers</title><link>https://barneyparker.com/posts/authenticate-users-with-azure-oidc-on-aws-application-load-balancers/</link><pubDate>Tue, 22 Sep 2020 18:36:55 +0000</pubDate><guid>https://barneyparker.com/posts/authenticate-users-with-azure-oidc-on-aws-application-load-balancers/</guid><description>&lt;p>Securing legacy applications which were deigned for internal use only has become a hot topic in these times of sudden remote working. There isn&amp;rsquo;t the time or money available to re-architect authentication, and its not always easy to provide remote accessibility to systems when the company doesn&amp;rsquo;t have the resources to implement it safely.&lt;/p>
&lt;p>Thankfully AWS Application Load Balancers provide a really simple method of adding an authentication layer to an application which doesn&amp;rsquo;t require any application code changes, and can be linked to an auth provider that you already have via OIDC, in this case the extremely common Office 365!&lt;/p>
&lt;h1 id="openid-connect-and-aws-application-load-balancers">OpenID Connect and AWS Application Load Balancers&lt;/h1>
&lt;p>OpenID Connect is an identity layer built on top of OAuth2 which can be used natively with AWS ALBs. An ALB requires a listener which has rules on how to forward traffic based on the incomming connection. In the vast majority of cases just a simple default listener rule to &amp;ldquo;forward&amp;rdquo; to the application target group is all that is used.&lt;/p>
&lt;p>In order to make use of OIDC, all we need to do is add a condition which checks for an OIDC auth cookie, and if its not present it starts the OIDC OAuth2 authentication flow!&lt;/p>
&lt;h1 id="creating-an-azure-app-registration">Creating an Azure App Registration&lt;/h1>
&lt;p>We need to create an application registration in Azure Active Directory. This will give us the various URLs and magic values we will need to be able to configure OIDC on our ALB.&lt;/p>
&lt;h2 id="create-the-app-registration">Create the App Registration&lt;/h2>
&lt;p>Log in to Azure at &lt;a href="https://portal.azure.com">portal.azure.com&lt;/a>&lt;/p>
&lt;p>Under &lt;strong>Azure Services&lt;/strong> click &lt;strong>Azure Active Directory&lt;/strong>&lt;/p>
&lt;p>In the left had menu, under &lt;strong>Manage&lt;/strong> click &lt;strong>App Registrations&lt;/strong>&lt;/p>
&lt;p>On the top bar, click &lt;strong>New registration&lt;/strong>&lt;/p>
&lt;p>Start by entering a name for your application. This helps identify the purpose of this configuration&lt;/p>
&lt;p>Under &lt;strong>Supported Account Types&lt;/strong> select the option &lt;strong>Accounts in this organizational directory only (&lt;org name> - Single tenant)&lt;/strong>. This will ensure only your companies&amp;rsquo; Office 365 users will be able to use our application.&lt;/p>
&lt;p>Finally, click the &lt;strong>Register&lt;/strong> button.&lt;/p>
&lt;p>Next we&amp;rsquo;re going to need to collect some information that we need to supply to the ALB.&lt;/p>
&lt;h2 id="create-the-client-secret">Create the Client Secret&lt;/h2>
&lt;p>In the left side menu, click &lt;strong>Certificates &amp;amp; secrets&lt;/strong>&lt;/p>
&lt;p>Under &lt;strong>Client Secrets&lt;/strong> click &lt;strong>New client secret&lt;/strong>&lt;/p>
&lt;p>Add a description to remind you what this secret is for. The application registration could be used for multiple applications, but each application should have its own secret, so the description will be helpful if the secret ever needs to be removed.&lt;/p>
&lt;p>Select an expiry. Most likely &lt;strong>Never&lt;/strong> is the best choice here.&lt;/p>
&lt;p>Finally click the &lt;strong>Add button&lt;/strong>&lt;/p>
&lt;p>The Client Secrets list will now contain your new secret. Copy the &lt;strong>Value&lt;/strong> field and add it to the following Terraform code:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">variable&lt;/span> &lt;span class="s2">&amp;#34;oidc_client_secret&amp;#34;&lt;/span> {
&lt;span class="n"> default&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;paste secret here&amp;gt;&amp;#34;&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="collect-our-other-information">Collect Our Other Information&lt;/h2>
&lt;p>On the left hand menu again, click &lt;strong>Overview&lt;/strong>&lt;/p>
&lt;p>In the &lt;strong>Essentials&lt;/strong> pane, copy the &lt;strong>Application (client) ID&lt;/strong> and paste it in to the following Terraform code:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">variable&lt;/span> &lt;span class="s2">&amp;#34;oidc_client_id&amp;#34;&lt;/span> {
&lt;span class="n"> default&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;paste client id here&amp;gt;&amp;#34;&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Next copy the &lt;strong>Directory (tenant) ID&lt;/strong> and paste in to the following Terraform code:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">variable&lt;/span> &lt;span class="s2">&amp;#34;oidc_tenant_id&amp;#34;&lt;/span> {
&lt;span class="n"> default&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;lt;paste tenant id here&amp;gt;&amp;#34;&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>And we have all the information we need to go ahead and implement OIDC in our listener.&lt;/p>
&lt;h2 id="redirect-uris">Redirect URIs&lt;/h2>
&lt;p>Part of the OAuth flow requires the load balancer to redirect to the Azure authorization endpoint. We need to configure this in Azure&lt;/p>
&lt;p>In the &lt;strong>Overview&lt;/strong> / &lt;strong>Essentials&lt;/strong> block, click the &lt;strong>Redirect URIs&lt;/strong> link&lt;/p>
&lt;p>Click the &lt;strong>Add Platform&lt;/strong> button&lt;/p>
&lt;p>Click &lt;strong>Web&lt;/strong>&lt;/p>
&lt;p>Enter the url to the root of the load balancer and append &lt;code>/oauth2/idpresponse&lt;/code>. e.g. if our app is at &lt;code>https://app.exmaple.com&lt;/code> this should show &lt;code>https://app.exmaple.com/oauth2/idpresponse&lt;/code>&lt;/p>
&lt;p>A little further down the page, under &lt;strong>Implicit Grants&lt;/strong> tick the option &lt;strong>Access Tokens&lt;/strong>, and untick &lt;strong>ID Tokens&lt;/strong>&lt;/p>
&lt;h1 id="create-our-oidc-alb-listeners">Create our OIDC ALB Listener(s)&lt;/h1>
&lt;p>For OIDC to work we need to be using HTTPS. It&amp;rsquo;s unwise to expose an application over HTTP anyway, so lets enforce this by adding a listener with a default action that redirects http traffic to https:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_lb_listener&amp;#34; &amp;#34;http&amp;#34;&lt;/span> {
&lt;span class="n"> load_balancer_arn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_lb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">application&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>
&lt;span class="n"> port&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;80&amp;#34;&lt;/span>
&lt;span class="n"> protocol&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;HTTP&amp;#34;&lt;/span>
&lt;span class="k">default_action&lt;/span> {
&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;redirect&amp;#34;&lt;/span>
&lt;span class="k">redirect&lt;/span> {
&lt;span class="n"> port&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;443&amp;#34;&lt;/span>
&lt;span class="n"> protocol&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;HTTPS&amp;#34;&lt;/span>
&lt;span class="n"> status_code&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;HTTP_301&amp;#34;&lt;/span>
}
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Now we need an HTTPS listener&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_lb_listener&amp;#34; &amp;#34;https&amp;#34;&lt;/span> {
&lt;span class="n"> load_balancer_arn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_lb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">application&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>
&lt;span class="n"> port&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;443&amp;#34;&lt;/span>
&lt;span class="n"> protocol&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;HTTPS&amp;#34;&lt;/span>
&lt;span class="n"> ssl_policy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;ELBSecurityPolicy-2016-08&amp;#34;&lt;/span>
&lt;span class="n"> certificate_arn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:iam::012345678999:server-certificate/test_cert_abc&amp;#34;&lt;/span>
&lt;span class="k">default_action&lt;/span> {
&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;authenticate-oidc&amp;#34;&lt;/span>
&lt;span class="k">authenticate_oidc&lt;/span> {
&lt;span class="n"> authorization_endpoint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://login.microsoftonline.com/${var.oidc_tenant_id}/oauth2/v2.0/authorize&amp;#34;&lt;/span>
&lt;span class="n"> client_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">oidc_client_id&lt;/span>
&lt;span class="n"> client_secret&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">oidc_client_secret&lt;/span>
&lt;span class="n"> issuer&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://login.microsoftonline.com/${var.oidc_tenant_id}/v2.0&amp;#34;&lt;/span>
&lt;span class="n"> token_endpoint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://login.microsoftonline.com/${var.oidc_tenant_id}/oauth2/v2.0/token&amp;#34;&lt;/span>
&lt;span class="n"> user_info_endpoint&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;https://graph.microsoft.com/oidc/userinfo&amp;#34;&lt;/span>
}
}
&lt;span class="k">default_action&lt;/span> {
&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;forward&amp;#34;&lt;/span>
&lt;span class="n"> target_group_arn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_lb_target_group&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">application&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="outbound-traffic-alb">Outbound Traffic ALB&lt;/h2>
&lt;p>An essential part to add is an Egress rule to the security group attached to the ALB for HTTPS traffic to allow the ALB to contact Azure to confirm tokens:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_security_group_rule&amp;#34; &amp;#34;applications_alb_egress_https&amp;#34;&lt;/span> {
&lt;span class="n"> from_port&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">443&lt;/span>
&lt;span class="n"> to_port&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">443&lt;/span>
&lt;span class="n"> protocol&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;tcp&amp;#34;&lt;/span>
&lt;span class="n"> cidr_blocks&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;0.0.0.0/0&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="n"> security_group_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_security_group&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">app_alb&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="notes">Notes&lt;/h1>
&lt;p>For brevity I have not included the Terraform code for the load balancer or target group, however this is relatively trivial to add.&lt;/p>
&lt;p>Once added you should find a couple of extra headers in the requests forwarded to our application:&lt;/p>
&lt;p>&lt;code>x-amzn-oidc-accesstoken&lt;/code> holds the full JSON Web Token (JWT)
&lt;code>x-amzn-oidc-data&lt;/code> is the user claims in JWT format
&lt;code>x-amzn-oidc-identity&lt;/code> contains the subject field (&lt;code>sub&lt;/code>) from the user info endpoint&lt;/p></description></item><item><title>Installing NPM Dependencies with Terraform</title><link>https://barneyparker.com/posts/installing-npm-dependencies-with-terraform/</link><pubDate>Tue, 23 Jun 2020 14:29:42 +0000</pubDate><guid>https://barneyparker.com/posts/installing-npm-dependencies-with-terraform/</guid><description>&lt;p>Terraform has the capability to create a zip file containing Function code, and for simpler functions that&amp;rsquo;s fine. For more complex functions it quickly becomes necessary to install dependencies to make avoid the need to write every line of code ourselves.&lt;/p>
&lt;p>The problem with Terraform is that it only wants to do things that have a Provider resource. &lt;code>npm install&lt;/code>ing dependencies needs to be done outside of this, and while we &lt;em>could&lt;/em> manually there is no way to guarantee we will remember to do it every time we apply.&lt;/p>
&lt;p>In order to keep this inside out Terraform workflow we need to abuse some resources a little.&lt;/p>
&lt;h2 id="triggering-dependency-installation">Triggering Dependency Installation&lt;/h2>
&lt;p>To convince Terraform to install our dependencies, we need to know&lt;/p>
&lt;ul>
&lt;li>Has &lt;code>package.json&lt;/code> changed?&lt;/li>
&lt;li>Has &lt;code>package-lock.json&lt;/code> changed?&lt;/li>
&lt;li>Do we have all the files we expected?&lt;/li>
&lt;/ul>
&lt;p>We also need to store something in the state file that we can compare to in the future.&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;null_resource&amp;#34; &amp;#34;lambda_dependencies&amp;#34;&lt;/span> {
&lt;span class="k">provisioner&lt;/span> &lt;span class="s2">&amp;#34;local-exec&amp;#34;&lt;/span> {
&lt;span class="n"> command&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;cd ${path.module}/src &amp;amp;&amp;amp; npm install&amp;#34;&lt;/span>
}
&lt;span class="n"> triggers&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;span class="n"> index&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">sha256&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/src/index.js&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="n"> package&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">sha256&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/src/package.json&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="n"> lock&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">sha256&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;${path.module}/src/package-lock.json&amp;#34;&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="n"> node&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">sha256&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">join&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;&amp;#34;,fileset(path.module, &amp;#34;src/**/*.js&amp;#34;&lt;/span>&lt;span class="p">)))&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>a &lt;code>null_resource&lt;/code> doesn&amp;rsquo;t do anything in itself, but it acts as a container that we can use to create one resource based on a set of trigger values.&lt;/p>
&lt;p>&lt;code>local-exec&lt;/code> allows us to run a system command, which in our case is to move in to the Lambda Function source directory and execute &lt;code>npm install&lt;/code>.&lt;/p>
&lt;p>We can set &lt;code>triggers&lt;/code> based on our requirement, i.e. has &lt;code>index.js&lt;/code>, &lt;code>package.json&lt;/code> or &lt;code>package-lock.json&lt;/code> changed by checking their sha256 signatures. We can also get a list of every .js file in the source directory, join them in to a single string and then get the sha256 of that string.&lt;/p>
&lt;p>These trigger values will be stored in the state file, so even if we run this on another machine we&amp;rsquo;re going to be able to tell if anything doesn&amp;rsquo;t match between the previous deployment and this one. If something doesn&amp;rsquo;t match, then the local-exec command will be executed, and our dependencies installed!&lt;/p>
&lt;h2 id="building-the-bundle">Building the Bundle&lt;/h2>
&lt;p>So far so good, but we need to enforce Terraform ordering in the dependency graph. If we just add an &lt;code>archive_file&lt;/code> data resource there&amp;rsquo;s no guarantee it will wait for the dependencies to be installed before creating the bundle.&lt;/p>
&lt;p>To fix this we add an intermediate resource called a &lt;code>null_data_source&lt;/code>. These resources take a bunch of inputs which can be computed form the outputs of other resources or variables. Until all of the inputs are satisfied Terraform wont allow anything depending on the &lt;code>null_data_source&lt;/code> to be created or modified.&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">data&lt;/span> &lt;span class="s2">&amp;#34;null_data_source&amp;#34; &amp;#34;wait_for_lambda_exporter&amp;#34;&lt;/span> {
&lt;span class="n"> inputs&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;span class="n"> lambda_dependency_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${null_resource.lambda_dependencies.id}&amp;#34;&lt;/span>
&lt;span class="n"> source_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.module}/src/&amp;#34;&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>lambda_dependency_id&lt;/code> input wont be known to Terraform until the previous &lt;code>null_resource&lt;/code> has completed - which means our &lt;code>npm install&lt;/code> has either been completed, or nothing had changed and it wasn&amp;rsquo;t required.&lt;/p>
&lt;p>The &lt;code>source_dir&lt;/code> input can be pre-computed by Terraform, and we will use this value to tell the &lt;code>archive_file&lt;/code> what to add to the bundle.&lt;/p>
&lt;p>Finally we create our &lt;code>archive_file&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">data&lt;/span> &lt;span class="s2">&amp;#34;archive_file&amp;#34; &amp;#34;lambda&amp;#34;&lt;/span> {
&lt;span class="n"> output_path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.module}/lambda-bundle.zip&amp;#34;&lt;/span>
&lt;span class="n"> source_dir&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${data.null_data_source.wait_for_lambda_exporter.outputs[&amp;#34;source_dir&amp;#34;]}&amp;#34;&lt;/span>
&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;zip&amp;#34;&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>So now we&amp;rsquo;re creating our &lt;code>archive_file.lambda&lt;/code> from the path given to us by &lt;code>null_data_source.wait_for_lambda_exporter&lt;/code>, which wont give us an answer until &lt;code>null_resource.lambda_dependencies&lt;/code> is satisfied, which ensures the correct dependency ordering is maintained!&lt;/p>
&lt;h2 id="less-than-ideal">Less than ideal&lt;/h2>
&lt;p>There is one minor issue with this method. On the first run there is no node modules, so the null_resource.lambda_dependencies.trigger.node value gets an initial sha256 value, and that causes npm install to be executed. This will then change the sha256 that would be computed to the src directory, and a second run would also create a new bundle. subsequent runs wont do this, but in an ideal world a single run should be all that is needed to converge our required to actual states.&lt;/p>
&lt;p>Its certainly not perfect, but if you can tolerate this one caveat, its a relatively simple way to include node dependencies without having to do anything more elaborate than a normal deployment.&lt;/p></description></item><item><title>HTTP APIs with Simple Lambda Functions</title><link>https://barneyparker.com/posts/http-apis-with-simple-lambda-functions/</link><pubDate>Fri, 19 Jun 2020 07:56:34 +0000</pubDate><guid>https://barneyparker.com/posts/http-apis-with-simple-lambda-functions/</guid><description>&lt;p>API Gateway has always been somewhat of a beast to configure, and while its incredibly flexible its also far more hassle than most people want or need. Terraform was also completely the wrong tool for deploying these because of the sheer number of resources required and the need to deploy APIs to a stage just didn&amp;rsquo;t quite fit with the way Terraform works.&lt;/p>
&lt;p>The new HTTP APIs, whilst almost completely un-google-able, are an amazingly simple was to expose Lambda Functions to the outside world. They&amp;rsquo;re easy to deploy, simple to define, have great 404 handling, built in JWT authentication, and an easy CORS config. Apart from the (currently!) missing service integrations, there&amp;rsquo;s literally nothing to dislike about them!&lt;/p>
&lt;h2 id="about-the-default-route">About The $default Route&lt;/h2>
&lt;p>HTTP APIs allow you to add a special route called &lt;code>$default&lt;/code> which will essentially capture anything that doesn&amp;rsquo;t already have a route handler attached to it. It sounds like an ideal fallback route, but there is one caveat in that it will also capture CORS &lt;code>OPTIONS&lt;/code> requests in some situations, which is definitely not what what I was expecting to happen with it.&lt;/p>
&lt;p>The documentation doesn&amp;rsquo;t state this particularly clearly, but instead of using a $default route, instead you should use a &lt;code>/{proxy+}&lt;/code> route to capture anything without a handler, which still allows the CORS requests to hit the HTTP APIs built-in handler without you needing to write any code.&lt;/p>
&lt;h2 id="defining-the-api">Defining the API&lt;/h2>
&lt;p>Creating an API is pretty simple and allows you to create the CORS config at the same time:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_apigatewayv2_api&amp;#34; &amp;#34;api&amp;#34;&lt;/span> {
&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;v2-http-api&amp;#34;&lt;/span>
&lt;span class="n"> protocol_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;HTTP&amp;#34;&lt;/span>
&lt;span class="k">cors_configuration&lt;/span> {
&lt;span class="n"> allow_credentials&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">false&lt;/span>
&lt;span class="n"> allow_headers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="n"> allow_methods&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="n"> allow_origins&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="n"> expose_headers&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;*&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;span class="n"> max_age&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">3600&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>This resource creates an HTTP type API in API Gateway, and adds an extremely open CORS policy. You would likely want to tie this down a little more, but this policy makes the API open everywhere which is easy for testing.&lt;/p>
&lt;h2 id="the-stage-with-logging">The Stage (With Logging)&lt;/h2>
&lt;p>Its good to be able to get request log files, which in AWS terms means a CloudWatch Log Group. In out case that needs to be attached to the API Stage. Incidentally HTTP APIs can be configured to &amp;ldquo;auto-deploy&amp;rdquo;, i.e. you don&amp;rsquo;t have to tell it to update the stage, just updating the API routes is enough.&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_cloudwatch_log_group&amp;#34; &amp;#34;api_logs&amp;#34;&lt;/span> {
&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/api/logs&amp;#34;&lt;/span>
&lt;span class="n"> retention_in_days&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">30&lt;/span>
}
&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_apigatewayv2_stage&amp;#34; &amp;#34;stage&amp;#34;&lt;/span> {
&lt;span class="n"> api_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_apigatewayv2_api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;$default&amp;#34;&lt;/span>
&lt;span class="n"> auto_deploy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">true&lt;/span>
&lt;span class="k">access_log_settings&lt;/span> {
&lt;span class="n"> destination_arn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_cloudwatch_log_group&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">api_logs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>
&lt;span class="n"> format&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">jsonencode&lt;/span>&lt;span class="p">(&lt;/span>
{
&lt;span class="n"> httpMethod&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;$context.httpMethod&amp;#34;&lt;/span>
&lt;span class="n"> ip&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;$context.identity.sourceIp&amp;#34;&lt;/span>
&lt;span class="n"> protocol&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;$context.protocol&amp;#34;&lt;/span>
&lt;span class="n"> requestId&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;$context.requestId&amp;#34;&lt;/span>
&lt;span class="n"> requestTime&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;$context.requestTime&amp;#34;&lt;/span>
&lt;span class="n"> responseLength&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;$context.responseLength&amp;#34;&lt;/span>
&lt;span class="n"> routeKey&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;$context.routeKey&amp;#34;&lt;/span>
&lt;span class="n"> status&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;$context.status&amp;#34;&lt;/span>
}
&lt;span class="p">)&lt;/span>
}
&lt;span class="k">lifecycle&lt;/span> {
&lt;span class="n"> ignore_changes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="k">deployment_id&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="k">default_route_settings&lt;/span>
&lt;span class="p">]&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>lifecycle&lt;/code> block at the bottom is necessary at the time of writing due to some slight oddities between the way HTTP APIs work and the way Terraform works. Basically it tells Terraform to ignore the &lt;code>deployment_id&lt;/code> and &lt;code>default_route_settings&lt;/code> fields if AWS says they don&amp;rsquo;t match whats in the state file.&lt;/p>
&lt;p>The &lt;code>access_log_settings.format&lt;/code> field specifies the log format. You can find more about that in the &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-logging.html">AWS Docs&lt;/a>&lt;/p>
&lt;p>There are some other resources available at this level to allow you to add a domain name and map this stage on to it, and to create a JWT Authorizer config, but for the sake of simplicity I will explore those in a separate article.&lt;/p>
&lt;h2 id="adding-a-route">Adding a Route&lt;/h2>
&lt;p>Adding Routes in V1 of API Gateway required a lot of work. Multiple path segments, a resource to handle the incoming request, one for the Integration, another for the integration response and multiple resources for potential return codes. There is a lot to love about all that including VTL templates doing a lot of work for you, and being able to avoid calling Lambda Functions altogether, but it really was an awful lot of resources, and Terraform is very verbose. There were even some timing issues with AWS eventual consistency that made deployment of infra code changes quite painful.&lt;/p>
&lt;p>HTTP APIs have done away with all of that in favour of a single resource for the route, and another one for the integration of the lambda function (with the option to add the JWT Authorizer)&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_apigatewayv2_route&amp;#34; &amp;#34;route&amp;#34;&lt;/span> {
&lt;span class="n"> api_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_apigatewayv2_api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;span class="n"> route_key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;GET /{proxy+}&amp;#34;&lt;/span>
&lt;span class="n"> target&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;integrations/${aws_apigatewayv2_integration.integration.id}&amp;#34;&lt;/span>
}
&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_apigatewayv2_integration&amp;#34; &amp;#34;integration&amp;#34;&lt;/span> {
&lt;span class="n"> api_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_apigatewayv2_api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">api&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">id&lt;/span>
&lt;span class="n"> integration_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;AWS_PROXY&amp;#34;&lt;/span>
&lt;span class="n"> connection_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;INTERNET&amp;#34;&lt;/span>
&lt;span class="n"> description&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;This is our {proxy+} integration&amp;#34;&lt;/span>
&lt;span class="n"> integration_method&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;POST&amp;#34;&lt;/span>
&lt;span class="n"> integration_uri&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_lambda_function&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">lambda&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">invoke_arn&lt;/span>
&lt;span class="n"> passthrough_behavior&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;WHEN_NO_MATCH&amp;#34;&lt;/span>
&lt;span class="k">lifecycle&lt;/span> {
&lt;span class="n"> ignore_changes&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="k">passthrough_behavior&lt;/span>
&lt;span class="p">]&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>The fields are fairly self-explanatory, the &lt;code>aws_apigatewayv2_route&lt;/code> attached a specific route to an API, and directs it to an integration.&lt;/p>
&lt;p>The &lt;code>aws_apigatewayv2_integration&lt;/code> tells API Gateway to forward the request to a Lambda function as an &lt;code>AWS_PROXY&lt;/code> integration type, using a &lt;code>POST&lt;/code> request to the Lambda service.&lt;/p>
&lt;p>At some point there will be more integrations (you can already pass through to another API via a VPC link for example) however Lambda is the only service currently integrated with HTTP APIs.&lt;/p>
&lt;p>Again, due to time of writing oddities in Terraform we need to add a &lt;code>lifecycle&lt;/code> block to ignore the &lt;code>passthrough_behaviour&lt;/code> field if it changes in AWS.&lt;/p>
&lt;h2 id="the-lambda-function-handler">The Lambda Function Handler&lt;/h2>
&lt;p>The Lambda Function is as simple as providing the ARN (shown above). Given this is a {proxy+} route it could capture multiple route requests, and in our case we will handle just the &lt;code>/&lt;/code> route and 404&amp;rsquo;s for anything not defined:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="nx">module&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">exports&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">handler&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="nx">async&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;Event: &amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">event&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="k">if&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">event&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">path&lt;/span> &lt;span class="o">===&lt;/span> &lt;span class="s1">&amp;#39;/&amp;#39;&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">statusCode&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">200&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">headers&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s1">&amp;#39;Content-Type&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;application/json&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">body&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">JSON&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">stringify&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">message&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Welcome&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">}),&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span> &lt;span class="k">else&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="nx">statusCode&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">404&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nx">headers&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;span class="s1">&amp;#39;Content-Type&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;application/json&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">},&lt;/span>
&lt;span class="nx">body&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="nx">JSON&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">stringify&lt;/span>&lt;span class="p">({&lt;/span>
&lt;span class="nx">message&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;Not Found&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">}),&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>You can drop this code in to the resources created in the article &lt;a href="https://barneyparker.com/posts/deploying-lambda-functions-with-terraform/">Deploying Lambda Functions with Terraform&lt;/a> and drop it straight in here.&lt;/p>
&lt;h2 id="accessing-the-api">Accessing the API&lt;/h2>
&lt;p>We&amp;rsquo;ve not added a domain name to map the API to, but we can still use the default url provided by API Gateway. The easiest way to get this is by adding an output to the code to tell Terraform to tell us what URL has been assigned to it:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">output&lt;/span> &lt;span class="s2">&amp;#34;api_url&amp;#34;&lt;/span> {
&lt;span class="n"> value&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_apigatewayv2_stage&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">stage&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">invoke_url&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>You should now be able to &lt;code>curl&lt;/code> the &lt;code>api_url&lt;/code> that Terraform gives you and see that for a &lt;code>GET&lt;/code> request on the &lt;code>/&lt;/code> route you see a welcome response, a &lt;code>404&lt;/code> response on all other requests, and an &lt;code>OPTIONS&lt;/code> request should return the CORS response to allow browsers to access the API!&lt;/p></description></item><item><title>Deploying Lambda Functions with Terraform</title><link>https://barneyparker.com/posts/deploying-lambda-functions-with-terraform/</link><pubDate>Tue, 16 Jun 2020 20:05:28 +0000</pubDate><guid>https://barneyparker.com/posts/deploying-lambda-functions-with-terraform/</guid><description>&lt;p>Serverless architectures are built from a collection of Managed service wired together to create a specific service. This could be anything from a shopping cart to a full ETL solution. The advantage of building applications this way is that each of the component parts are fully managed by AWS. That means you (either the developer or company) don&amp;rsquo;t have to worry about them functioning, you just need to make sure they&amp;rsquo;re wired together correctly.&lt;/p>
&lt;p>Managed services are great, but these service only offer basic functionality such as queues or databases. What they can&amp;rsquo;t do is know how your business actually adds value to the world.&lt;/p>
&lt;p>Lambda functions are the Serverless super-hero because they let you run your own code without ever having to worry about how the server operates (yes, there are servers!) or even how how to install the software that runs your code.&lt;/p>
&lt;p>There are a lot of ways to deploy AWS infrastructure, and even more for Serverless infrastructure, but my personal favourite is Terraform. Its designed to allow you to write your infrastructure as code (IaC) and is surprisingly flexible. It also allows you to interact with non-AWS services which can really help tie everything together.&lt;/p>
&lt;p>Terraform can be a little verbose, but its very easy to understand, and very simple to work with. On the face of it Terraform doesn&amp;rsquo;t obviously lend itself to Serverless, but that&amp;rsquo;s because it designed to give you extremely low-level access to APIs in the form of Resources.&lt;/p>
&lt;p>In this article I will show you the minimal set of code required to deploy a lambda function. Its not going to be production-ready, or even do anything very impressive, but its going to introduce at an entry level the minimal components required.&lt;/p>
&lt;h2 id="about-terraform">About Terraform&lt;/h2>
&lt;p>Terraform works by creating a direct-graph of resources. The resource dependency order can be determined by using outputs from one resource as the inputs to another. Terraform can then deploy resources which have no outstanding dependencies recursively until all resources have been deployed with their dependencies satisfied.&lt;/p>
&lt;p>Terraform then creates a state file which it uses on further deployments to identify which specific resources in AWS it should be adding, deleting or modifying. State files can be stored in local files or remotely, but for now we will just keep them locally.&lt;/p>
&lt;h2 id="terraform-meta-resources">Terraform Meta-Resources&lt;/h2>
&lt;p>Terraform is incredibly configurable, and to do that it happily eats it own dog-food by configuring with Terraform resources! This is the content of a file I like to call &lt;code>meta.tf&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">terraform&lt;/span> {
&lt;span class="n"> required_version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;&amp;gt; 0.12&amp;#34;&lt;/span>
}
&lt;span class="k">provider&lt;/span> &lt;span class="s2">&amp;#34;aws&amp;#34;&lt;/span> {
&lt;span class="n"> region&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;eu-west-1&amp;#34;&lt;/span>
&lt;span class="n"> version&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> &amp;#34;&amp;gt;&lt;/span>&lt;span class="o">=&lt;/span> &lt;span class="m">2&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="m">61&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="err">&amp;#34;&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>terraform&lt;/code> block tells Terraform to use a version no less that 0.12.0. While older versions are available, this version added some functionality that makes it a little easier to code and so is a good base.&lt;/p>
&lt;p>The &lt;code>provider&lt;/code> block tells Terraform that we want to use AWS resources, we want to place them in the &lt;code>eu-west-1&lt;/code> region and we want to use a provider version no less than 2.61.0&lt;/p>
&lt;h2 id="creating-the-lambda-function-bundle">Creating the Lambda Function Bundle&lt;/h2>
&lt;p>A Lambda Function needs some code to run. The code can be in a &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html">variety of languages&lt;/a> but in this case we&amp;rsquo;re going to use Javascript. Here is some simple code that we&amp;rsquo;re going to deploy:&lt;/p>
&lt;p>To supply the code to the Lambda Function it needs to be in a Zip file. Terraform can create these zip files for us, and for simplicity we&amp;rsquo;re going to define the Function code inline:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">data&lt;/span> &lt;span class="s2">&amp;#34;archive_file&amp;#34; &amp;#34;lambda&amp;#34;&lt;/span> {
&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;zip&amp;#34;&lt;/span>
&lt;span class="n"> output_path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${path.module}/.terraform/lambda.zip&amp;#34;&lt;/span>
&lt;span class="k">source&lt;/span> {
&lt;span class="n"> filename&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;index.js&amp;#34;&lt;/span>
&lt;span class="n"> content&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="err">&amp;lt;&amp;lt;-&lt;/span>&lt;span class="k">EOF&lt;/span>
&lt;span class="n"> module.exports.handler&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> async (event, context)&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="err">&amp;gt;&lt;/span> {
&lt;span class="k">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;EVENT: \n&amp;#34;&lt;/span> &lt;span class="err">+&lt;/span> &lt;span class="k">JSON&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">stringify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">event&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">null&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">2&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="k">console&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">log&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;CONTEXT: \n&amp;#34;&lt;/span> &lt;span class="err">+&lt;/span> &lt;span class="k">JSON&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">stringify&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="k">context&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">null&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">2&lt;/span>&lt;span class="p">))&lt;/span>
&lt;span class="k">return&lt;/span> &lt;span class="k">Promise&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">resolve&lt;/span>&lt;span class="p">()&lt;/span>
}
&lt;span class="k">EOF&lt;/span>
}
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Lines 7-10 will be written to a file called &lt;code>index.js&lt;/code> which will be added to a zip file called lambda.zip. ${path.module} will be interpolated as the local path during deployment. The code exports a function called &lt;code>handler&lt;/code>. Keep these in mind as they will be useful when we actually deploy our function.&lt;/p>
&lt;p>The code is extremely simple: it&amp;rsquo;s going to print out the Event and Context objects which will be passed to it upon execution. This is how the function will get information related to its execution.&lt;/p>
&lt;h2 id="the-log-file">The Log File&lt;/h2>
&lt;p>Since the Lambda Function is running inside of AWS, we need some way to capture its output. The Lambda service automatically captures anything which would normally be sent to stdin and stderr and store that in a CloudWatch Log Stream. Although the Lambda service will create the Log Group, its best practice to create this yourself and set an expiry period so that the logs are automatically cleared out after a period of time:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_cloudwatch_log_group&amp;#34; &amp;#34;lambda_log_group&amp;#34;&lt;/span> {
&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/aws/lambda/${aws_lambda_function.lambda.function_name}&amp;#34;&lt;/span>
&lt;span class="n"> retention_in_days&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">30&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>All Lambda functions log to a log group called &lt;code>/aws/lambda/&amp;lt;lambda name&amp;gt;&lt;/code> and so we&amp;rsquo;re creating that and capturing the name from the Lambda Function resource (created very soon!)&lt;/p>
&lt;p>We also set the logs to expire after 30 days. Logs are pretty cheap, but if the function is called a lot or it generates a lot of log output the cost can quickly mount up so after a short period we want AWS to delete them on our behalf.&lt;/p>
&lt;h2 id="permissions">Permissions&lt;/h2>
&lt;p>In order to be able to interact with any other service, AWS resources need an IAM Role which can be &amp;ldquo;assumed&amp;rdquo; by the calling service. In our case that&amp;rsquo;s the Lambda service:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_role&amp;#34; &amp;#34;lambda&amp;#34;&lt;/span> {
&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;simplest_lambda_role&amp;#34;&lt;/span>
&lt;span class="n"> assume_role_policy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="err">&amp;lt;&amp;lt;-&lt;/span>&lt;span class="k">EOF&lt;/span>
{
&lt;span class="s2">&amp;#34;Version&amp;#34;: &amp;#34;2012-10-17&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;Statement&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">[&lt;/span>
{
&lt;span class="s2">&amp;#34;Effect&amp;#34;: &amp;#34;Allow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;Action&amp;#34;: &amp;#34;sts:AssumeRole&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;Principal&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> {
&lt;span class="s2">&amp;#34;Service&amp;#34;: &amp;#34;lambda.amazonaws.com&amp;#34;&lt;/span>
}
}
&lt;span class="p">]&lt;/span>
}
&lt;span class="k">EOF&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>The role needs to have policies associated with it in order to define what its allowed to do. In our case the only thing we need permission to do is to create Log Streams within the Log Group, and write to them. Policies can be stand-alone, re-usable policies, or in-line policies specific to this function. For simplicity in this case we&amp;rsquo;re going to create an in-line policy:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_role_policy&amp;#34; &amp;#34;logging&amp;#34;&lt;/span> {
&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Cloudwatch-Logs&amp;#34;&lt;/span>
&lt;span class="n"> role&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_iam_role&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">lambda&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">name&lt;/span>
&lt;span class="n"> policy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="err">&amp;lt;&amp;lt;-&lt;/span>&lt;span class="k">EOF&lt;/span>
{
&lt;span class="s2">&amp;#34;Statement&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">[&lt;/span>
{
&lt;span class="s2">&amp;#34;Action&amp;#34;&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="s2">&amp;#34;logs:CreateLogGroup&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;logs:CreateLogStream&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;logs:PutLogEvents&amp;#34;&lt;/span>
&lt;span class="p">],&lt;/span>
&lt;span class="s2">&amp;#34;Effect&amp;#34;: &amp;#34;Allow&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="s2">&amp;#34;Resource&amp;#34;: &amp;#34;arn:aws:logs:*:*:*&amp;#34;&lt;/span>
}
&lt;span class="p">]&lt;/span>
}
&lt;span class="k">EOF&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="the-lambda-function">The Lambda Function&lt;/h2>
&lt;p>At last we have all the pieces in place to be able to create our Lambda Function resource. Here we will give it a name, point it to our code bundle, tell it what runtime to use and what exported function to execute and what Role it should be assuming to give itself the correct permissions:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_lambda_function&amp;#34; &amp;#34;lambda&amp;#34;&lt;/span> {
&lt;span class="n"> function_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;simplest_lambda_function&amp;#34;&lt;/span>
&lt;span class="n"> filename&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">archive_file&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">lambda&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">output_path&lt;/span>
&lt;span class="n"> source_code_hash&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">archive_file&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">lambda&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">output_base64sha256&lt;/span>
&lt;span class="n"> runtime&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;nodejs12.x&amp;#34;&lt;/span>
&lt;span class="n"> handler&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;index.handler&amp;#34;&lt;/span>
&lt;span class="n"> role&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_iam_role&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">lambda&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>
}
&lt;/code>&lt;/pre>&lt;/div>&lt;p>Terraform hides some nasty internal values such as ARNs and file hashes by taking the outputs from some of the resources we created earlier as inputs to this resource. In doing so we have also implied the dependencies of resources and helped Terraform work out what order things should be deployed.&lt;/p>
&lt;h2 id="deploying-the-function">Deploying the Function&lt;/h2>
&lt;p>To start the deployment process, Terraform needs to be initialised with the command:&lt;/p>
&lt;p>&lt;code>terraform init&lt;/code>&lt;/p>
&lt;p>This will ensure we&amp;rsquo;re using a valid version of Terraform and download the providers specified in &lt;code>meta.tf&lt;/code> (or a default if we didn&amp;rsquo;t specify a version). All of its internal &amp;ldquo;stuff&amp;rdquo; will be stored in a directory called &lt;code>.terraform&lt;/code>&lt;/p>
&lt;p>To see what Terraform is going to do execute the following command:&lt;/p>
&lt;p>&lt;code>terraform plan&lt;/code>&lt;/p>
&lt;p>You should get an output something like:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="k">Refreshing&lt;/span> &lt;span class="k">Terraform&lt;/span> &lt;span class="k">state&lt;/span> &lt;span class="k">in&lt;/span>&lt;span class="err">-&lt;/span>&lt;span class="k">memory&lt;/span> &lt;span class="k">prior&lt;/span> &lt;span class="k">to&lt;/span> &lt;span class="k">plan&lt;/span>&lt;span class="p">...&lt;/span>
&lt;span class="k">The&lt;/span> &lt;span class="k">refreshed&lt;/span> &lt;span class="k">state&lt;/span> &lt;span class="k">will&lt;/span> &lt;span class="k">be&lt;/span> &lt;span class="k">used&lt;/span> &lt;span class="k">to&lt;/span> &lt;span class="k">calculate&lt;/span> &lt;span class="k">this&lt;/span> &lt;span class="k">plan&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">but&lt;/span> &lt;span class="k">will&lt;/span> &lt;span class="k">not&lt;/span> &lt;span class="k">be&lt;/span>
&lt;span class="k">persisted&lt;/span> &lt;span class="k">to&lt;/span> &lt;span class="k">local&lt;/span> &lt;span class="k">or&lt;/span> &lt;span class="k">remote&lt;/span> &lt;span class="k">state&lt;/span> &lt;span class="k">storage&lt;/span>&lt;span class="p">.&lt;/span>
&lt;span class="k">data&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">archive_file&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">lambda&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="k">Refreshing&lt;/span> &lt;span class="k">state&lt;/span>&lt;span class="p">...&lt;/span>
&lt;span class="err">------------------------------------------------------------------------&lt;/span>
&lt;span class="k">An&lt;/span> &lt;span class="k">execution&lt;/span> &lt;span class="k">plan&lt;/span> &lt;span class="k">has&lt;/span> &lt;span class="k">been&lt;/span> &lt;span class="k">generated&lt;/span> &lt;span class="k">and&lt;/span> &lt;span class="k">is&lt;/span> &lt;span class="k">shown&lt;/span> &lt;span class="k">below&lt;/span>&lt;span class="p">.&lt;/span>
&lt;span class="k">Resource&lt;/span> &lt;span class="k">actions&lt;/span> &lt;span class="k">are&lt;/span> &lt;span class="k">indicated&lt;/span> &lt;span class="k">with&lt;/span> &lt;span class="k">the&lt;/span> &lt;span class="k">following&lt;/span> &lt;span class="k">symbols&lt;/span>&lt;span class="err">:&lt;/span>
&lt;span class="err">+&lt;/span> &lt;span class="k">create&lt;/span>
&lt;span class="k">Terraform&lt;/span> &lt;span class="k">will&lt;/span> &lt;span class="k">perform&lt;/span> &lt;span class="k">the&lt;/span> &lt;span class="k">following&lt;/span> &lt;span class="k">actions&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="c1">
&lt;/span>&lt;span class="c1">
&lt;/span>&lt;span class="c1"> # aws_cloudwatch_log_group.lambda_log_group will be created
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="err">+&lt;/span> &lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_cloudwatch_log_group&amp;#34; &amp;#34;lambda_log_group&amp;#34;&lt;/span> {
&lt;span class="n"> + arn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/aws/lambda/simplest_lambda_function&amp;#34;&lt;/span>
&lt;span class="n"> + retention_in_days&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">30&lt;/span>
}&lt;span class="c1">
&lt;/span>&lt;span class="c1">
&lt;/span>&lt;span class="c1"> # aws_iam_role.lambda will be created
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="err">+&lt;/span> &lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_role&amp;#34; &amp;#34;lambda&amp;#34;&lt;/span> {
&lt;span class="n"> + arn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + assume_role_policy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">jsonencode&lt;/span>&lt;span class="p">(&lt;/span>
{
&lt;span class="n"> + Statement&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="err">+&lt;/span> {
&lt;span class="n"> + Action&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;sts:AssumeRole&amp;#34;&lt;/span>
&lt;span class="n"> + Effect&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Allow&amp;#34;&lt;/span>
&lt;span class="n"> + Principal&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;span class="n"> + Service&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;lambda.amazonaws.com&amp;#34;&lt;/span>
}
}&lt;span class="p">,&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="n"> + Version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;2012-10-17&amp;#34;&lt;/span>
}
&lt;span class="p">)&lt;/span>
&lt;span class="n"> + create_date&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + force_detach_policies&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">false&lt;/span>
&lt;span class="n"> + id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + max_session_duration&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">3600&lt;/span>
&lt;span class="n"> + name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;simplest_lambda_role&amp;#34;&lt;/span>
&lt;span class="n"> + path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/&amp;#34;&lt;/span>
&lt;span class="n"> + unique_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
}&lt;span class="c1">
&lt;/span>&lt;span class="c1">
&lt;/span>&lt;span class="c1"> # aws_iam_role_policy.logging will be created
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="err">+&lt;/span> &lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_iam_role_policy&amp;#34; &amp;#34;logging&amp;#34;&lt;/span> {
&lt;span class="n"> + id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Cloudwatch-Logs&amp;#34;&lt;/span>
&lt;span class="n"> + policy&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">jsonencode&lt;/span>&lt;span class="p">(&lt;/span>
{
&lt;span class="n"> + Statement&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="err">+&lt;/span> {
&lt;span class="n"> + Action&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>
&lt;span class="err">+&lt;/span> &lt;span class="s2">&amp;#34;logs:CreateLogGroup&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="err">+&lt;/span> &lt;span class="s2">&amp;#34;logs:CreateLogStream&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="err">+&lt;/span> &lt;span class="s2">&amp;#34;logs:PutLogEvents&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="p">]&lt;/span>
&lt;span class="n"> + Effect&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Allow&amp;#34;&lt;/span>
&lt;span class="n"> + Resource&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:logs:*:*:*&amp;#34;&lt;/span>
}&lt;span class="p">,&lt;/span>
&lt;span class="p">]&lt;/span>
}
&lt;span class="p">)&lt;/span>
&lt;span class="n"> + role&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;simplest_lambda_role&amp;#34;&lt;/span>
}&lt;span class="c1">
&lt;/span>&lt;span class="c1">
&lt;/span>&lt;span class="c1"> # aws_lambda_function.lambda will be created
&lt;/span>&lt;span class="c1">&lt;/span> &lt;span class="err">+&lt;/span> &lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_lambda_function&amp;#34; &amp;#34;lambda&amp;#34;&lt;/span> {
&lt;span class="n"> + arn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + filename&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;./.terraform/lambda.zip&amp;#34;&lt;/span>
&lt;span class="n"> + function_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;simplest_lambda_function&amp;#34;&lt;/span>
&lt;span class="n"> + handler&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;index.handler&amp;#34;&lt;/span>
&lt;span class="n"> + id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + invoke_arn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + last_modified&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + memory_size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">128&lt;/span>
&lt;span class="n"> + publish&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="kt">false&lt;/span>
&lt;span class="n"> + qualified_arn&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + reserved_concurrent_executions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="err">-&lt;/span>&lt;span class="m">1&lt;/span>
&lt;span class="n"> + role&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + runtime&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;nodejs12.x&amp;#34;&lt;/span>
&lt;span class="n"> + source_code_hash&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> &amp;#34;TFoUgCw/pW7Sy3/gY/TB3AtGzXoqRjN1L6YT066iPGg&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="err">&amp;#34;&lt;/span>
&lt;span class="n"> + source_code_size&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="n"> + timeout&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">3&lt;/span>
&lt;span class="n"> + version&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
&lt;span class="err">+&lt;/span> &lt;span class="k">tracing_config&lt;/span> {
&lt;span class="n"> + mode&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="k">known&lt;/span> &lt;span class="k">after&lt;/span> &lt;span class="k">apply&lt;/span>&lt;span class="p">)&lt;/span>
}
}
&lt;span class="k">Plan&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="m">4&lt;/span> &lt;span class="k">to&lt;/span> &lt;span class="k">add&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">0&lt;/span> &lt;span class="k">to&lt;/span> &lt;span class="k">change&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="m">0&lt;/span> &lt;span class="k">to&lt;/span> &lt;span class="k">destroy&lt;/span>&lt;span class="p">.&lt;/span>
&lt;span class="err">------------------------------------------------------------------------&lt;/span>
&lt;span class="k">Note&lt;/span>&lt;span class="err">:&lt;/span> &lt;span class="k">You&lt;/span> &lt;span class="k">didn&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="k">t&lt;/span> &lt;span class="k">specify&lt;/span> &lt;span class="k">an&lt;/span> &lt;span class="s2">&amp;#34;-out&amp;#34;&lt;/span> &lt;span class="k">parameter&lt;/span> &lt;span class="k">to&lt;/span> &lt;span class="k">save&lt;/span> &lt;span class="k">this&lt;/span> &lt;span class="k">plan&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">so&lt;/span> &lt;span class="k">Terraform&lt;/span>
&lt;span class="k">can&lt;/span>&lt;span class="err">&amp;#39;&lt;/span>&lt;span class="k">t&lt;/span> &lt;span class="k">guarantee&lt;/span> &lt;span class="k">that&lt;/span> &lt;span class="k">exactly&lt;/span> &lt;span class="k">these&lt;/span> &lt;span class="k">actions&lt;/span> &lt;span class="k">will&lt;/span> &lt;span class="k">be&lt;/span> &lt;span class="k">performed&lt;/span> &lt;span class="k">if&lt;/span>
&lt;span class="s2">&amp;#34;terraform apply&amp;#34;&lt;/span> &lt;span class="k">is&lt;/span> &lt;span class="k">subsequently&lt;/span> &lt;span class="k">run&lt;/span>&lt;span class="p">.&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>This shows that Terraform will create 4 new resources:&lt;/p>
&lt;ul>
&lt;li>The Log Group&lt;/li>
&lt;li>An IAM Role&lt;/li>
&lt;li>An Inline IAM Policy attached to the Role&lt;/li>
&lt;li>Our Lambda Function&lt;/li>
&lt;/ul>
&lt;p>To actually create resources in AWS, execute:&lt;/p>
&lt;p>&lt;code>terraform apply&lt;/code>&lt;/p>
&lt;p>When prompted, enter &lt;code>yes&lt;/code>&lt;/p>
&lt;h2 id="testing-our-function">Testing our Function&lt;/h2>
&lt;p>To invoke our function from the command line, execute:&lt;/p>
&lt;pre>&lt;code>`aws lambda invoke --function-name simplest_lambda_function /dev/null --log-type Tail --query 'LogResult' --output text | base64 -d`
&lt;/code>&lt;/pre>&lt;p>which should respond with something similar to:&lt;/p>
&lt;div class="highlight">&lt;pre class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="err">START&lt;/span> &lt;span class="err">RequestId:&lt;/span> &lt;span class="err">ab&lt;/span>&lt;span class="mi">934699-7094-4829-8&lt;/span>&lt;span class="err">cee-aecd&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="err">c&lt;/span>&lt;span class="mi">193583&lt;/span> &lt;span class="err">Version:&lt;/span> &lt;span class="err">$LATEST&lt;/span>
&lt;span class="mi">2020-06-16&lt;/span>&lt;span class="err">T&lt;/span>&lt;span class="mi">16&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="mi">58&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="mf">46.098&lt;/span>&lt;span class="err">Z&lt;/span> &lt;span class="err">ab&lt;/span>&lt;span class="mi">934699-7094-4829-8&lt;/span>&lt;span class="err">cee-aecd&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="err">c&lt;/span>&lt;span class="mi">193583&lt;/span> &lt;span class="err">INFO&lt;/span> &lt;span class="err">EVENT:&lt;/span>
&lt;span class="p">{}&lt;/span>
&lt;span class="mi">2020-06-16&lt;/span>&lt;span class="err">T&lt;/span>&lt;span class="mi">16&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="mi">58&lt;/span>&lt;span class="err">:&lt;/span>&lt;span class="mf">46.098&lt;/span>&lt;span class="err">Z&lt;/span> &lt;span class="err">ab&lt;/span>&lt;span class="mi">934699-7094-4829-8&lt;/span>&lt;span class="err">cee-aecd&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="err">c&lt;/span>&lt;span class="mi">193583&lt;/span> &lt;span class="err">INFO&lt;/span> &lt;span class="err">CONTEXT:&lt;/span>
&lt;span class="p">{&lt;/span>
&lt;span class="nt">&amp;#34;callbackWaitsForEmptyEventLoop&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="kc">true&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;functionVersion&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;$LATEST&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;functionName&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;simplest_lambda_function&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;memoryLimitInMB&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;128&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;logGroupName&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;/aws/lambda/simplest_lambda_function&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;logStreamName&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;2020/06/16/[$LATEST]759235165de84d3bbe7d3a87a7bd2db4&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;invokedFunctionArn&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;arn:aws:lambda:eu-west-1:522711524578:function:simplest_lambda_function&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;span class="nt">&amp;#34;awsRequestId&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;ab934699-7094-4829-8cee-aecd2c193583&amp;#34;&lt;/span>
&lt;span class="p">}&lt;/span>
&lt;span class="err">END&lt;/span> &lt;span class="err">RequestId:&lt;/span> &lt;span class="err">ab&lt;/span>&lt;span class="mi">934699-7094-4829-8&lt;/span>&lt;span class="err">cee-aecd&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="err">c&lt;/span>&lt;span class="mi">193583&lt;/span>
&lt;span class="err">REPORT&lt;/span> &lt;span class="err">RequestId:&lt;/span> &lt;span class="err">ab&lt;/span>&lt;span class="mi">934699-7094-4829-8&lt;/span>&lt;span class="err">cee-aecd&lt;/span>&lt;span class="mi">2&lt;/span>&lt;span class="err">c&lt;/span>&lt;span class="mi">193583&lt;/span> &lt;span class="err">Duration:&lt;/span> &lt;span class="mf">2.86&lt;/span> &lt;span class="err">ms&lt;/span> &lt;span class="err">Billed&lt;/span> &lt;span class="err">Duration:&lt;/span> &lt;span class="mi">100&lt;/span> &lt;span class="err">ms&lt;/span> &lt;span class="err">Memory&lt;/span> &lt;span class="err">Size:&lt;/span> &lt;span class="mi">128&lt;/span> &lt;span class="err">MB&lt;/span> &lt;span class="err">Max&lt;/span> &lt;span class="err">Memory&lt;/span> &lt;span class="err">Used:&lt;/span> &lt;span class="mi">63&lt;/span> &lt;span class="err">MB&lt;/span> &lt;span class="err">Init&lt;/span> &lt;span class="err">Duration:&lt;/span> &lt;span class="mf">129.60&lt;/span> &lt;span class="err">ms&lt;/span>
&lt;/code>&lt;/pre>&lt;/div>&lt;p>While the command was a little long-winded, this is the log data you will also find in the CloudWatch Log Group. We can see the invocation id ab934699-7094-4829-8cee-aecd2c193583 and the content of the event and context objects. Finally we can see the &lt;code>REPORT&lt;/code> line with tells us&lt;/p>
&lt;ul>
&lt;li>The function took 2.86ms to run&lt;/li>
&lt;li>It will be billed at 100ms (always rounded up to the nearest 100ms)&lt;/li>
&lt;li>It was allocated 128MB of memory&lt;/li>
&lt;li>At its peak, it actually used a total of 63MB of memory&lt;/li>
&lt;li>It &amp;ldquo;Cold-Start&amp;rdquo; initialisation time was 129.6ms&lt;/li>
&lt;/ul>
&lt;p>You can use these values to tune the function which I will address in a future post.&lt;/p>
&lt;h2 id="cleaning-up">Cleaning Up&lt;/h2>
&lt;p>Since this was just a simple demo, you probably want to remove all these resources at some point. To do so execute:&lt;/p>
&lt;p>&lt;code>terraform destroy&lt;/code>&lt;/p>
&lt;h2 id="final-remarks">Final Remarks&lt;/h2>
&lt;p>This function doesn&amp;rsquo;t do a great deal, and right now we can only invoke it from either the command line or from the AWS Lambda console, but it covers the basics of using Terraform to get it, and all its supporting resources deployed.&lt;/p>
&lt;p>For more information see the official &lt;a href="https://www.terraform.io/">Terraform&lt;/a> site.&lt;/p></description></item><item><title>About Barney Parker</title><link>https://barneyparker.com/about/</link><pubDate>Mon, 15 Jun 2020 00:00:00 +0000</pubDate><guid>https://barneyparker.com/about/</guid><description>&lt;p>Barney is pretty special :)&lt;/p></description></item><item><title>World Simplest HTML5 WYSIWYG Inline Editor</title><link>https://barneyparker.com/posts/world-simplest-html5-wysiwyg-inline-editor/</link><pubDate>Mon, 05 Aug 2013 00:00:00 +0000</pubDate><guid>https://barneyparker.com/posts/world-simplest-html5-wysiwyg-inline-editor/</guid><description>&lt;p>I am pretty new to web/JavaScript development, and often quickly reach for Google in order to find out how to do things.&lt;/p>
&lt;p>Recently I needed to add the ability to do some pretty basic in-page editing of content. I didn&amp;rsquo;t want to use iFrame&amp;rsquo;s, i didn&amp;rsquo;t want a hugely featured editor, literally some pretty basic tools to be able to edit content, bold, italic, h1, h2 and so on.&lt;/p>
&lt;p>After a fair bit of Googling, it seemed that there wasn&amp;rsquo;t a hope! all the plug in&amp;rsquo;s i found just seems unnecessarily complicated so, in a fit of self-importance, i decided i could do it myself. i&amp;rsquo;d end up with the editor i wanted, and code that i fully understood. Granted, it wasn&amp;rsquo;t likely to be an easy job, but hey, why would i code anything if there was an easier option?&lt;/p>
&lt;p>To my surprise, as it turns out, it&amp;rsquo;s so simple, it&amp;rsquo;s almost a joke! all the tools are already available, all you need to do it tie them together with some really simple JavaScript, and it works!&lt;/p>
&lt;p>There are two things you need, the first is&lt;/p>
&lt;p>{% highlight javascript %}contenteditable{% endhighlight %}&lt;/p>
&lt;p>&lt;strong>contenteditable&lt;/strong> tells the browser that this item (a div for example) can be edited inline. You can do all the normal text editing here with ctrl-b for bold, ctrl-i for italics etc, but it&amp;rsquo;s fairly limited.&lt;/p>
&lt;p>The second thing you need is a special function called&lt;/p>
&lt;p>{% highlight javascript %}execCommand{% endhighlight %}&lt;/p>
&lt;p>&lt;strong>execCommand&lt;/strong> actually allows us to do some fairly complex stuff with the contents of the **contenteditable **area, and all in a really compliant way!&lt;/p>
&lt;p>If you want to read what it can do in detail, check out &lt;a title="the Mozilla Docs" href="https://developer.mozilla.org/en-US/docs/Rich-Text_Editing_in_Mozilla#Executing_Commands" target="_blank">The Mozilla Docs&lt;/a>&lt;/p>
&lt;p>Essentially, &lt;strong>execCommand&lt;/strong> takes three parameters:&lt;/p>
&lt;p>&lt;strong>Command Name&lt;/strong> – the name of a command&lt;/p>
&lt;p>&lt;strong>ShowDefaultUI&lt;/strong> – not implemented, and best set to false&lt;/p>
&lt;p>&lt;strong>ValueArgument&lt;/strong> – optional for some commands so for example, using the command &amp;lsquo;bold&amp;rsquo; will make the currently selected text turn bold with a &lt;b> tag!&lt;/p>
&lt;p>ValueArgument can be set to null in all but one case. When you want to set a tag for a line of text (h1, h2 for example) you need to use the &lt;strong>formatBlock&lt;/strong>  command, and put the tag you want in the ValueArgument (without angle-braces).&lt;/p>
&lt;p>Now this is simple enough, but to make life even easier, a single click handler and a data-attribute on each link allow us to have one very simple function to handle everything.&lt;/p>
&lt;p>The demo uses bootstrap and jQuery, but only to make the code a little more brief, and could all easily be done in plain old JavaScript.&lt;/p>
&lt;p>[CodePen height=300 show=result href=idjCG user=barney-parker ]&lt;/pre>&lt;/p>
&lt;p>EDIT: CompuSam suggested using {% highlight javascript %}href='javascript:void(0)'{% endhighlight %} instead of {% highlight javascript %}href&amp;rsquo;#'{% endhighlight %} to stop and unwanted page movements etc&amp;hellip;.&lt;/p></description></item><item><title/><link>https://barneyparker.com/drafts/design-for-3am/</link><pubDate>Mon, 01 Jan 0001 00:00:00 +0000</pubDate><guid>https://barneyparker.com/drafts/design-for-3am/</guid><description/></item></channel></rss>