<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Jason Butz</title>
    <description>The latest articles on Forem by Jason Butz (@jbutz).</description>
    <link>https://forem.com/jbutz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F54043%2F12b45243-d7ef-440c-9590-829243650acd.jpg</url>
      <title>Forem: Jason Butz</title>
      <link>https://forem.com/jbutz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/jbutz"/>
    <language>en</language>
    <item>
      <title>New DynamoDB Key Feature &amp; Why It Matters</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Mon, 24 Nov 2025 22:00:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/new-dynamodb-key-feature-why-it-matters-38ln</link>
      <guid>https://forem.com/aws-builders/new-dynamodb-key-feature-why-it-matters-38ln</guid>
      <description>&lt;p&gt;AWS &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/11/amazon-dynamodb-multi-attribute-composite-keys-global-secondary-indexes/" rel="noopener noreferrer"&gt;announced multi-attribute composite keys for DynamoDB global secondary indexes on November 19th&lt;/a&gt;. That's a lot of words, but what does it actually mean? It means it's easier to pull data out of DynamoDB in ways you hadn't planned initially, without a bunch of extra work. To fully explain why, I need to explain keys in DynamoDB. I'll assume you know that DynamoDB is a NoSQL database, that the "database" is called a &lt;em&gt;table&lt;/em&gt;, and the table is a collection of items, which you can think of as records or rows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Primary Key, Partition Key, Sort Key
&lt;/h2&gt;

&lt;p&gt;You'll hear many names used for different keys in DynamoDB. It all makes sense, but it can be confusing. Every item in your table has a primary key, as you are probably used to with relational databases such as MySQL, PostgreSQL, Microsoft SQL Server, Oracle, and many others. That primary key is a unique value you can use to retrieve a particular item. In DynamoDB, the primary key can consist of either a single attribute or two attributes.&lt;/p&gt;

&lt;p&gt;Your primary key always has a partition key (PK), sometimes called a hash key or hash attribute. The partition key is used by DynamoDB to distribute your items across partitions, enabling the performance for which DynamoDB is known. There are a whole lot of best practices for choosing a partition key, but you don't need to know them right now.&lt;/p&gt;

&lt;p&gt;Your primary key might include a sort key (SK), sometimes called a range key or range attribute. If your primary key has a sort key, then it is known as a &lt;em&gt;composite primary key&lt;/em&gt;. With a composite primary key, multiple items can have the same partition key. The combination of the partition key and sort key is what must be unique. The sort key determines how values returned are sorted when you query for the items with a given partition key.&lt;/p&gt;

&lt;p&gt;Below is a screenshot using sample data AWS provides that shows an example table with only a partition key. In the example, the partition key is the user's username, &lt;em&gt;LoginAlias&lt;/em&gt;, as shown in the screenshot. This allows you to easily retrieve a user's information if you know their login alias.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj5f2402gmscowm8pusy4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj5f2402gmscowm8pusy4.png" alt="Table showing data, with the columns labeled " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The following screenshot shows the same data, with the partition key as the user's first name and the sort key as their last name. This allows you to easily find users by name, for example, all users named &lt;em&gt;Jane&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk473ioqgjvtautr64ui6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk473ioqgjvtautr64ui6.png" alt="Table showing data, with the columns labeled " width="800" height="469"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Get, Query, Scan
&lt;/h2&gt;

&lt;p&gt;Before I get into key structures, I have to cover the three primary operations for reading data from DynamoDB. You can either use a &lt;code&gt;GetItem&lt;/code&gt; action to read a single item, use the &lt;code&gt;Query&lt;/code&gt; API to retrieve multiple items based on primary key values, or use the &lt;code&gt;Scan&lt;/code&gt; operation.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;GetItem&lt;/code&gt;, you provide the entire primary key, and DynamoDB returns your item. It's very straightforward and &lt;em&gt;very&lt;/em&gt; fast.&lt;/p&gt;

&lt;p&gt;For a query, you must know the partition key value, but you can perform more complex comparisons for the sort key value, as well as for other attributes on the items. Using the screenshot above as an example, you could query for all users with the first name "Jane" and a last name that begins with "R", or for all users with the first name "Jane" who have "software" as a skill. Query operations are generally very fast, thanks to knowing exactly which partition to access for the data.&lt;/p&gt;

&lt;p&gt;Scan operations don't require you to know the primary key, but they are much slower and far less efficient than other operations. You can apply filter expressions to limit the data returned, but this happens after the data is read. A scan operation reads out 1MB of data from your table, then applies any filter expression. If that doesn't return your results, you will need to paginate and scan the next batch of data. In effect, you are reading your entire table to get the data you want. You should avoid scans whenever possible.&lt;/p&gt;

&lt;p&gt;After reviewing these operations, you can see why we want to focus on &lt;code&gt;GetItem&lt;/code&gt; and query operations, but both require us to know the partition key. Now you can see why key structure matters. There is one more topic to cover before we really focus on key structures: indexes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Secondary Indexes
&lt;/h2&gt;

&lt;p&gt;DynamoDB has a feature called &lt;em&gt;secondary indexes&lt;/em&gt;. They are effectively copies of your table that you read from the same way, but organized differently. There are two types: &lt;em&gt;global secondary indexes&lt;/em&gt; (GSI) and &lt;em&gt;local secondary indexes&lt;/em&gt; (LSI). A local secondary index uses the same partition key as your base table, but you can configure a different sort key. A global secondary index can have a different partition key and sort key from your base table.&lt;/p&gt;

&lt;p&gt;There are additional considerations with secondary indexes, but those are outside the scope of this discussion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Structures
&lt;/h2&gt;

&lt;p&gt;You've seen how important the partition key is for your data. A standard line you used to hear from AWS about DynamoDB performance was the importance of "well-structured queries". If you don't know your partition key, your query is not well-structured. GSIs let you define new partition keys and offer a lot of flexibility, but you still need to know the partition key for each GSI.&lt;/p&gt;

&lt;p&gt;The importance of your partition key is why you need to understand your data and data access patterns before developing your DynamoDB table. If you will always know your user's username once they log in, then it may be a good partition key to use. If you have a multi-tenant application and will always know the tenant ID associated with a user, then that may be a good partition key.&lt;/p&gt;

&lt;p&gt;Sort keys are where things get interesting. Using someone's last name as a sort key works for a basic example, but real applications are rarely that simple. Perhaps you are tracking details and audit records for different pieces of equipment. That's two different kinds of data about your equipment, in the same table. Your equipment ID could serve as your partition key, and you could use "details" as your sort key for the equipment information. For your audit items, you can use version numbers or timestamps as part of your sort key to make them sortable, something like &lt;code&gt;audit_v1&lt;/code&gt; or &lt;code&gt;audit_v20251121T121314Z&lt;/code&gt;. This lets you keep track of your past audits while also being able to retrieve the most recent one by sorting values. You do need to be careful of possible issues when sorting numbers as strings. For example, &lt;code&gt;audit_v10&lt;/code&gt; will come after &lt;code&gt;audit_v1&lt;/code&gt; but before &lt;code&gt;audit_v2&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With complex use cases, you may end up using a &lt;em&gt;composite sort key&lt;/em&gt; that isn't a single value but instead multiple values concatenated. AWS's documentation uses the following as an example for a table listing geographical data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[country]#[region]#[state]#[county]#[city]#[neighborhood]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of structure lets you use &lt;code&gt;begins_with&lt;/code&gt;, &lt;code&gt;between&lt;/code&gt;, and greater than/less than operators to retrieve related groups of data. You can use your partition key and query for all data related to King County, Washington (where Amazon's HQ is) by querying for a sort key beginning with &lt;code&gt;usa#northwest#wa#king#&lt;/code&gt; (USA for the country, Northwest for the region, WA meaning Washington state, and King meaning King County). Note that I included the &lt;code&gt;#&lt;/code&gt; at the end of the value, which prevents us from picking up extra values due to partial matches.&lt;/p&gt;

&lt;p&gt;By combining these ideas, you can have multiple formats for sort keys in the same table. I had one application with a single DynamoDB table containing 12-15 different item types. We had numerous organizations in the same table, with their organization ID as the partition key and sort keys such as &lt;code&gt;organization&lt;/code&gt;, &lt;code&gt;user#[user guid]#detail&lt;/code&gt;, &lt;code&gt;user#[user guid]#permissions&lt;/code&gt;, and more.&lt;/p&gt;

&lt;p&gt;The downside to these composite sort keys is that you have to build them yourself when you create your items, setting them as an attribute. AWS doesn't do it for you. Now imagine you're adding a new GSI and want a new composite sort key. Now you have to update every item in your table with the correct value for that sort key. It becomes very challenging; hence, you generally want an in-depth understanding of how your data will be accessed before defining the key structures for your DynamoDB tables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Multi-Attribute Key Schemas
&lt;/h2&gt;

&lt;p&gt;This is where the AWS announcement comes in. Multi-attribute key schemas are a new feature of GSIs. Instead of needing to manually concatenate and backfill values, the DynamoDB service can do that for you. Instead of writing a process to iterate over your table and update every item with your new attribute for a new partition key and sort key in a new GSI, you tell AWS which attributes you want used in the keys and in what order, and AWS handles it. AWS doesn't actually add a new attribute, but instead lets you retrieve items based on multiple attributes.&lt;/p&gt;

&lt;p&gt;AWS uses &lt;code&gt;TOURNAMENT#WINTER2024#REGION#NA-EAST&lt;/code&gt; as an example composite key, representing scores from a game tournament. Instead, you specify up to four attributes for your partition key and up to four for your sort key. In this example, &lt;code&gt;tournamentId&lt;/code&gt; and &lt;code&gt;region&lt;/code&gt; are the partition key. When reading data from the GSI, instead of referencing an attribute you defined with a composite value, you specify the attributes as part of your query. This keeps your data looking cleaner and saves you a headache.&lt;/p&gt;

&lt;p&gt;This means instead of defining an item like the code below, with composite keys as attributes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matchId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;match-001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tournamentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WINTER2024&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NA-EAST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SEMIFINALS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;bracket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPPER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;player1Id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;101&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// Synthetic keys needed for GSI&lt;/span&gt;
  &lt;span class="na"&gt;GSI_PK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`TOURNAMENT#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;tournamentId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#REGION#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;GSI_SK&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;round&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;bracket&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;matchId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You define the attributes on your GSI and use your item as-is, without extra concatenation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;matchId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;match-001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;tournamentId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WINTER2024&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;NA-EAST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;round&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SEMIFINALS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;bracket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;UPPER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;player1Id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;101&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;matchDate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2024-01-18&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You are limited to four attributes in the partition key and four attributes in the sort key. This applies only to global secondary indexes (GSIs). It is not available for your base table or local secondary indexes (LSIs).&lt;/p&gt;

&lt;p&gt;If you want to learn more about what this means for key structures, AWS added a &lt;a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.DesignPattern.MultiAttributeKeys.html" rel="noopener noreferrer"&gt;page to their documentation covering multi-attribute key patterns&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;I'm excited about multi-attribute key schemas because they make DynamoDB easier to use and more flexible without requiring complex backfill work. I've always had to caution people about DynamoDB because of the importance of well-designed key structures. They're still important, but adding new GSIs with different key structures is easier now, and that's a significant improvement.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>nosql</category>
    </item>
    <item>
      <title>Is flat-rate CloudFront worth it?</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Wed, 19 Nov 2025 16:00:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/is-flat-rate-cloudfront-worth-it-2bgn</link>
      <guid>https://forem.com/aws-builders/is-flat-rate-cloudfront-worth-it-2bgn</guid>
      <description>&lt;p&gt;AWS just released flat-rate plans for CloudFront and related services, wrapping together WAF, Route53, CloudFront Function, logging, and more. Sounds tempting, especially with a free tier. You can read more about AWS's &lt;a href="https://aws.amazon.com/blogs/networking-and-content-delivery/introducing-flat-rate-pricing-plans-with-no-overages/" rel="noopener noreferrer"&gt;announcement on their blog&lt;/a&gt;. Is it really worth it?&lt;/p&gt;

&lt;p&gt;AWS has announced four pricing tiers: Free, Pro ($15/month), Business ($200/month), and Premium ($1,000/month). Of course, each has different capabilities and limits included.&lt;/p&gt;

&lt;p&gt;I had thought about creating a big table to compare what's available, but it quickly got too complicated. You can check out &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/flat-rate-pricing-plan.html" rel="noopener noreferrer"&gt;AWS's non-marketing comparison of features by pricing tiers in their documentation&lt;/a&gt;. I do want to call out a few things that are included and excluded from some of the lower tiers. I'm not going to focus as much on the Business and Premium tiers because, of course, they have the best features, and at those price points, you've already shown some willingness to get them. It's the lower tiers where you miss out on some valuable features that aren't expensive, and those are the tiers where small creators and companies are going to focus.&lt;/p&gt;

&lt;p&gt;One interesting thing included in the free tier is WAF rules. In the free tier, you get 5; in the pro tier, you get 25. Those rules usually cost $1 per month. That's already making the pro price tag appealing. Include the WordPress/PHP/SQL DB security rules in the pro and above tiers, and that looks like a good deal. But what are you giving up?&lt;/p&gt;

&lt;p&gt;On the free and pro tier plans, you don't have access to &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-vpc-origins.html" rel="noopener noreferrer"&gt;private VPC origins&lt;/a&gt;. Instead, your origins, the systems CloudFront is connecting to, must be in a public VPC or generally accessible to the internet. If you want access to your site or application via the CDN, you probably don't want other public paths available to the system. You can use security groups and Origin Access Control (OAC) to limit access to only CloudFront, but not having the system publicly addressable is better. Additionally, you'll probably also have to &lt;a href="https://aws.amazon.com/vpc/pricing/" rel="noopener noreferrer"&gt;pay for public IPv4 addresses from AWS&lt;/a&gt;. It's not a massive cost, but it can add up over time.&lt;/p&gt;

&lt;p&gt;You also can't use custom origin request or response header policies in the free or pro tier plans. The default request policies are fine for most cases, but the response header policies matter for security headers such as &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP" rel="noopener noreferrer"&gt;Content Security Policy (CSP)&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS" rel="noopener noreferrer"&gt;Cross-Origin Resource Sharing (CORS)&lt;/a&gt;. Most modern websites and applications should have CSP and CORS headers configured.&lt;/p&gt;

&lt;p&gt;One CloudFront feature I was surprised to see available only in the premium tier is CloudFront origin groups, which provide origin failover to enable high availability. You configure multiple origins that can return the same content into a group with primary and secondary origins. When the primary origin returns specific errors, CloudFront retrieves the content from the secondary origin. I remember the &lt;a href="https://aws.amazon.com/message/12721/" rel="noopener noreferrer"&gt;us-east-1 outage in December 2021&lt;/a&gt;; my client experienced a major P1 incident, with most of their applications returning errors. The issue most users encountered was that S3 couldn't return the assets needed to load the web frontends, though other issues users didn't see also existed. CloudFront origin groups can fix that. I even wrote a blog post about addressing that for my site (&lt;a href="https://jasonbutz.info/2023/01/website-cdn/" rel="noopener noreferrer"&gt;Building a Resilient Static Website on AWS&lt;/a&gt;). It's not &lt;em&gt;that&lt;/em&gt; complicated and is within reach of smaller businesses and creators who want to ensure there is at least a friendly error message for users visiting their site during an AWS outage.&lt;/p&gt;

&lt;p&gt;AWS Hero &lt;a href="https://theburningmonk.com/" rel="noopener noreferrer"&gt;Yan Cui&lt;/a&gt; wrote &lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7396825126069096448/" rel="noopener noreferrer"&gt;a post on LinkedIn&lt;/a&gt; discussing this new flat-rate pricing, including highlighting concerns with it. He pointed out that the marketing material references "serverless edge compute," but that is misleading. Only CloudFront functions are included in the flat-rate pricing plans; Lambda@Edge functions are not. That's a significant difference in capabilities. If I'm reading the documentation right, you can't even use Lambda@Edge functions with these flat-rate plans. He also calls out the language around not incurring overage charges, noting that, instead, performance may be reduced, such as throttling or requiring you to change your pricing structure. Yan extrapolates on that last point, plainly calling out the trade-off: If you're below your quota, you may be wasting money, but if you're above your quota, you risk throttling and/or losing your pricing structure.&lt;/p&gt;

&lt;p&gt;What's more important to you? Stable pricing or availability and elasticity? One of the main selling points of the major cloud providers is availability and elasticity. If price were the only concern, you could probably host your site or application elsewhere much more cheaply. Traditional hosting and data center companies still exist, and you can still host applications on a server running in your home or business. Both will have a much more consistent pricing structure, but will lack the availability and elasticity of the cloud.&lt;/p&gt;

&lt;p&gt;I understand AWS wanting to offer consistent pricing if that's what customers are asking for, but this whole thing stinks of Azure's tendency to lock features behind tiers across its services. I hope that's not the direction AWS is heading. I appreciate the true building-block approach that AWS provides.&lt;/p&gt;

&lt;p&gt;If these flat-rate plans provide what you and your use case need, and the consistent pricing is a big selling point for you, then by all means use them. AWS capabilities are solid regardless of how you pay for them, but if you need flexibility and availability and can handle some bill fluctuation, stick with pay-as-you-go pricing for CloudFront.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Debugging ECR Private Pull Through Cache</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Wed, 12 Nov 2025 09:17:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/debugging-ecr-private-pull-through-cache-5d17</link>
      <guid>https://forem.com/aws-builders/debugging-ecr-private-pull-through-cache-5d17</guid>
      <description>&lt;p&gt;I completely missed that AWS added pull through cache rules for private ECR (Elastic Container Repository) repositories earlier this year. If you're an organization making use of publicly available container images this goes a long way towards helping you avoid throttling issues, accelerating deployments, and letting you have some confidence that the container versions you depend on will remain available.&lt;/p&gt;

&lt;p&gt;One of my coworkers was having trouble with the pull through cache, his application that was running in ECS wasn't pulling the most recent container image for the &lt;code&gt;latest&lt;/code&gt; tag like he expected. Even worse, it couldn't pull down images if he used a different tag. I don't know how he got the initial version of the image into ECR, but I wouldn't have expected his issues to start up after things seemed to be working.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🕐 &lt;strong&gt;TL;DR&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Make sure your ECS task execution role has the &lt;code&gt;ecr:BatchImportUpstreamImage&lt;/code&gt; and &lt;code&gt;ecr:CreateRepository&lt;/code&gt; actions on repositories matching your pull through cache prefix. Once you have it working, refine the policy to the least access necessary.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When he tried different to use new tags in his task definition he got errors in the deployment logs like the one below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service Sample-service was unable to place a task. Reason: CannotPullContainerError: pull image manifest has been retried 7 time(s): failed to resolve ref 012345678910.dkr.ecr.us-east-1.amazonaws.com/docker-hub/sample/sample:4.13.5: 012345678910.dkr.ecr.us-east-1.amazonaws.com/docker-hub/sample/sample:4.13.5: not found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The image didn't exist in the ECR repository, so it makes sense the service wasn't able to find it, but the whole point of a pull through cache is that the image gets pulled. I decided I needed to test that the pull through cache could work at all.&lt;/p&gt;

&lt;p&gt;I set up my local environment with the right AWS credentials and then used the &lt;em&gt;View Push Commands&lt;/em&gt; button for the repository to get the command I needed to set up access to the ECR repository. That let me try pulling the image locally through the cache.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Setup ECR Repository Access
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 012345678910.dkr.ecr.us-east-1.amazonaws.com
# Pull the container image
docker pull 012345678910.dkr.ecr.us-east-1.amazonaws.com/docker-hub/sample/sample:4.13.5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That worked, and the new images for that tag showed up in ECR. So the cache was working, but something was preventing it working within ECS. Sounds like permissions issues.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://giphy.com/gifs/sherlock-bbc-one-bbc1-l4JyTxAx1jFvzKKaY" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc50kol90sidwz160b69e.gif" alt="Sherlock Holmes, looking suspicious" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Since the container images are run as ECS tasks, I pulled up the ECS task IAM execution role. It has the permissions necessary to pull the container images, but does it have the permissions for the pull through cache? It did not!&lt;/p&gt;

&lt;p&gt;There are two permissions potentially needed for pulling through images for the cache:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ecr:BatchImportUpstreamImage&lt;/code&gt; to actually pull down the image and update your private ECR repository&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ecr:CreateRepository&lt;/code&gt; to create a new repository if you specify a brand new image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since my coworker wasn't planning to pull down entirely new container images, he only needs the first permission. I updated the IAM role with the policy below and had things working!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "VisualEditor0",
    "Effect": "Allow",
    "Action": "ecr:BatchImportUpstreamImage",
    "Resource": "arn:aws:ecr:us-east-1:012345678910:repository/docker-hub/*"
  }]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://giphy.com/gifs/sherlock-bbc-one-bbc1-26FLa6peMp3ZNzKnu" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fsld77b7kpbsm211z2fjk.gif" alt="Sherlock Holmes, smiling" width="512" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it! We had things working. We just needed to give the ECS task execution role the additional permissions needed for the repositories matching his pull through cache prefix. He is pulling multiple container images, so the wildcard isn't too excessive. It it were a single image the policy could be updated for that single repository.&lt;/p&gt;

&lt;p&gt;We're still not sure why the &lt;a href="https://docs.aws.amazon.com/AmazonECR/latest/userguide/pull-through-cache.html" rel="noopener noreferrer"&gt;once per 24 hour update alluded to in the AWS documentation&lt;/a&gt; isn't working, but that's a mystery for another day.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>containers</category>
    </item>
    <item>
      <title>JavaScript Lambda Runtime Benchmarking</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Thu, 22 May 2025 12:30:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/javascript-lambda-runtime-benchmarking-4i0o</link>
      <guid>https://forem.com/aws-builders/javascript-lambda-runtime-benchmarking-4i0o</guid>
      <description>&lt;p&gt;Last month, I tried out using &lt;a href="https://dev.to/aws-builders/javascript-lambda-functions-using-a-bun-custom-runtime-46hm"&gt;Bun as a custom JavaScript Lambda runtime&lt;/a&gt; and was surprised by the performance difference between Bun and AWS's managed Node.js runtime. The Node runtime had a shorter cold start time, and the invocations had a shorter duration. The cold start times weren't surprising given AWS's caching and performance optimizations, but the difference in the invocation durations was surprising.&lt;/p&gt;

&lt;p&gt;Since then, I have been working on a better benchmark to compare performance between AWS's Node.js runtime and the custom Bun and Deno runtimes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Setup &amp;amp; Comparison
&lt;/h2&gt;

&lt;p&gt;I originally generated a JWT as a simple test of the runtime's compute performance. For this new comparison, I continued to use hashing but instead generated 50 SHA3-512 hashes per invocation. I felt that generating a single hash was too small a test, and generating 50 hashes would result in a better comparison.&lt;/p&gt;

&lt;p&gt;For Bun and Deno, I wrote the handler in TypeScript, taking advantage of the runtime's capabilities. For the Node.js handler, I used plain JavaScript. If the runtime had a hashing function built in, I used that, hoping for the best performance. For Bun, I used the CryptoHasher function. For Deno and Node.js, I used the Node.js crypto package and the createHash function. The source code for the handler functions is below.&lt;/p&gt;

&lt;p&gt;I set up the Bun custom runtime using Lambda Layers, like I had previously. The Deno custom runtime is now container-based; it used Lambda Layers when I last used it. I used the &lt;a href="https://docs.deno.com/examples/aws_lambda_tutorial/#step-2%253A-create-a-dockerfile" rel="noopener noreferrer"&gt;Dockerfile that is available in Deno's documentation&lt;/a&gt; and only modified the filename for the handler.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// bun-handler.ts&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HASH_NUMBER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;HASH_NUMBER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;Bun&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CryptoHasher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha3-512&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;hasher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// deno-handler.ts&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HASH_NUMBER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;Deno&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;HASH_NUMBER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha3-512&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;hasher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="c1"&gt;// node-handler.js&lt;/span&gt;
&lt;span class="c1"&gt;//&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createHash&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;HASH_NUMBER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;HASH_NUMBER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha3-512&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;hasher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hasher&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I built an AWS CDK project to deploy the different Lambda functions, the infrastructure necessary to trigger the functions, and the dashboard for the aggregated results. I've created a GitHub repository with everything if you want to try out the experiment or see how I used the custom runtimes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jbutz/poc-aws-lambda-benchmark" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fopengraph.githubassets.com%2Fe30336e3d6003d20425ab913985f211a722d3e1cd5845836273e165466490b20%2Fjbutz%2Fpoc-aws-lambda-benchmark" alt="GitHub jbutz/poc-aws-lambda-benchmark" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used an event-based approach to invoke the Lambda functions and run the hashing code. Each Lambda was triggered based on messages being added to an SQS queue. I spaced out when messages would become available in the queue using the delay property to avoid having a surge of cold starts and parallel invocations. I wanted to be sure I got invocations that used an already running instance of the function. After all, having a readily available function and everything cached is often when you have your best performance.&lt;/p&gt;

&lt;p&gt;Every couple of hours, a scheduled Lambda function added 30 messages to each SQS queue with a steadily increasing delay until the max delay of 5 minutes was reached.&lt;/p&gt;

&lt;p&gt;I enabled JSON-formatted logs for the functions; I found I was able to parse out the metrics I needed easily in CloudWatch. I relied on the metrics reported by Lambda in the platform.report log entry for this analysis.&lt;/p&gt;

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

&lt;p&gt;I used the duration and initialization duration measurements provided by the Lambda service for these results. Both numbers are in milliseconds. Remember that 1000 milliseconds (ms) is 1 second. The duration is how long it took the function to process the event. The initialization duration is how long it took the function's Init phase to complete. This happens when a new execution environment is being set up. Technically, this may not include the time required to download the source code, but that is difficult to measure. I'm simplifying things and considering the initialization duration and the cold start duration equivalent for this comparison.&lt;/p&gt;

&lt;p&gt;I had 1326 invocations per runtime, totaling 3978 invocations overall. Each runtime experienced around 70 cold starts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ Percentiles are values below which a given percentage of all the values in the dataset exist. So if 15.190 is our value for the 50th percentile, then 50% of our values are below 15.190. You can learn more on &lt;a href="https://en.wikipedia.org/wiki/Percentile" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/Statistics-definitions.html" rel="noopener noreferrer"&gt;Amazon CloudWatch's documentation&lt;/a&gt;. In the tables below percentiles are written as "p10", "p25", etc. Where "p10" means "the 10th percentile", "p25" means "the 25th percentile", and so on.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Duration&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
  &lt;tr&gt;
    &lt;th&gt;Runtime&lt;/th&gt;
    &lt;th&gt;Invocations&lt;/th&gt;
    &lt;th&gt;Avg&lt;/th&gt;
    &lt;th&gt;p10&lt;/th&gt;
    &lt;th&gt;p25&lt;/th&gt;
    &lt;th&gt;p50&lt;/th&gt;
    &lt;th&gt;p75&lt;/th&gt;
    &lt;th&gt;p90&lt;/th&gt;
  &lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
  &lt;tr&gt;
    &lt;td&gt;Bun&lt;/td&gt;
    &lt;td&gt;1326&lt;/td&gt;
    &lt;td&gt;50.513&lt;/td&gt;
    &lt;td&gt;2.610&lt;/td&gt;
    &lt;td&gt;6.855&lt;/td&gt;
    &lt;td&gt;15.190&lt;/td&gt;
    &lt;td&gt;37.531&lt;/td&gt;
    &lt;td&gt;68.230&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Deno&lt;/td&gt;
    &lt;td&gt;1326&lt;/td&gt;
    &lt;td&gt;13.708&lt;/td&gt;
    &lt;td&gt;2.167&lt;/td&gt;
    &lt;td&gt;2.359&lt;/td&gt;
    &lt;td&gt;6.692&lt;/td&gt;
    &lt;td&gt;15.699&lt;/td&gt;
    &lt;td&gt;19.836&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Node&lt;/td&gt;
    &lt;td&gt;1326&lt;/td&gt;
    &lt;td&gt;21.290&lt;/td&gt;
    &lt;td&gt;1.951&lt;/td&gt;
    &lt;td&gt;2.156&lt;/td&gt;
    &lt;td&gt;8.052&lt;/td&gt;
    &lt;td&gt;20.831&lt;/td&gt;
    &lt;td&gt;56.711&lt;/td&gt;
  &lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When it comes to the invocation durations, Deno had the best averages, followed by the Node.js runtime. On average, Bun took more than twice the time to generate the hashes.&lt;/p&gt;

&lt;p&gt;Looking across the percentiles, Deno and Node were pretty close all the way out to the 75th percentile. Even at the 75th percentile, Deno was about 5ms faster, which you probably wouldn't notice. Looking at Bun, up until about the 50th percentile, it wasn't drastically slower. You're not going to notice 4ms, or even 10ms. Let's be honest, you're probably not going to notice a 50ms difference. To put that in perspective, a human blink takes between 100ms and 400ms. But, with AWS, you're billed for that time, and it can add up at the end of your billing cycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Initialization Duration&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
  &lt;tr&gt;
    &lt;th&gt;Runtime&lt;/th&gt;
    &lt;th&gt;Cold Starts&lt;/th&gt;
    &lt;th&gt;Avg&lt;/th&gt;
    &lt;th&gt;p10&lt;/th&gt;
    &lt;th&gt;p25&lt;/th&gt;
    &lt;th&gt;p50&lt;/th&gt;
    &lt;th&gt;p75&lt;/th&gt;
    &lt;th&gt;p90&lt;/th&gt;
  &lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
  &lt;tr&gt;
    &lt;td&gt;Bun&lt;/td&gt;
    &lt;td&gt;70&lt;/td&gt;
    &lt;td&gt;547.651&lt;/td&gt;
    &lt;td&gt;500.075&lt;/td&gt;
    &lt;td&gt;512.706&lt;/td&gt;
    &lt;td&gt;527.048&lt;/td&gt;
    &lt;td&gt;546.935&lt;/td&gt;
    &lt;td&gt;603.223&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Deno&lt;/td&gt;
    &lt;td&gt;72&lt;/td&gt;
    &lt;td&gt;267.474&lt;/td&gt;
    &lt;td&gt;184.607&lt;/td&gt;
    &lt;td&gt;191.996&lt;/td&gt;
    &lt;td&gt;203.221&lt;/td&gt;
    &lt;td&gt;218.661&lt;/td&gt;
    &lt;td&gt;297.237&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;Node&lt;/td&gt;
    &lt;td&gt;71&lt;/td&gt;
    &lt;td&gt;152.014&lt;/td&gt;
    &lt;td&gt;145.555&lt;/td&gt;
    &lt;td&gt;147.338&lt;/td&gt;
    &lt;td&gt;151.067&lt;/td&gt;
    &lt;td&gt;154.884&lt;/td&gt;
    &lt;td&gt;159.869&lt;/td&gt;
  &lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Looking at the initialization durations, the Node runtime was the fastest on average. I don't find this surprising, since I imagine AWS has put a lot of effort into minimizing cold start times. Bun was the slowest on average; I expect that is because it uses Lambda Layers, where Deno uses a container.&lt;/p&gt;

&lt;p&gt;Across the percentiles, there is a very small gap in the values for the Node runtime. To me, this indicates AWS has ensured these initialization durations for their Node runtime are fairly consistent. The difference between the 10th percentile and the 90th percentile is about 14ms. That's astoundingly consistent. The difference for Deno is about 30ms, and for Bun is 56ms. The initialization duration for the Bun runtime is consistently more than half a second. For Deno, it's under a quarter of a second more than 75% of the time.&lt;/p&gt;

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

&lt;p&gt;AWS's Node.js runtime for Lambda is very performant, and I am sure AWS has put a lot of work into ensuring that is the case. It has relatively consistent initialization durations and can generally be quite fast.&lt;/p&gt;

&lt;p&gt;Deno's container-based custom runtime is currently more performant than Bun's Lambda Layer-based approach, for both initialization and invocation. That said, Bun's approach is easier to work with and deploy. Deno's approach means you may end up building a container for every Lambda function, or you end up with larger Lambda functions where you use a fraction of the files included. I'm impressed by how performant the Node.js runtime is, when Bun and Deno are meant to be more performant JavaScript runtimes.&lt;/p&gt;

&lt;p&gt;These runtimes all perform well, especially if your application rarely has cold starts. If you need to gain every last bit of performance out of each Lambda invocation, your choice may matter, but if you're building a fairly basic API, I think you could choose any of these runtimes. Focus on the features you need and the developer experience you want.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>javascript</category>
    </item>
    <item>
      <title>JavaScript Lambda Functions Using a Bun Custom Runtime</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Wed, 23 Apr 2025 12:30:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/javascript-lambda-functions-using-a-bun-custom-runtime-46hm</link>
      <guid>https://forem.com/aws-builders/javascript-lambda-functions-using-a-bun-custom-runtime-46hm</guid>
      <description>&lt;p&gt;I've previously &lt;a href="https://dev.to/aws-builders/enhancing-aws-lambda-security-with-deno-31am"&gt;tried out Lambda functions with a custom runtime using Deno&lt;/a&gt;, and it had great security and convenience benefits. But Deno isn't the only alternative to the Node.js runtime. &lt;a href="https://bun.sh/" rel="noopener noreferrer"&gt;Bun&lt;/a&gt; is a more recent entrant to the space, but it has an impressive number of features, including not requiring TypeScript to be transpiled, and it makes a lot of claims around speed. Bun also has &lt;a href="https://github.com/oven-sh/bun/tree/main/packages/bun-lambda" rel="noopener noreferrer"&gt;everything for a custom Lambda runtime buried in its GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom Lambda Runtimes
&lt;/h2&gt;

&lt;p&gt;Custom runtimes are distributed as Lambda Layers, and the OS-only Lambda runtimes are used as their base. The OS-only runtimes are Amazon Linux 2 and Amazon Linux 2023 and are the foundation on which the rest of the runtime builds. The custom runtimes have certain APIs and processes that they have to implement to work with the Lambda service and be able to respond to events.&lt;/p&gt;

&lt;p&gt;I visualize and think of Lambda runtimes and layers like Docker or other OCI-compatible containers. I wouldn't be surprised if that's actually how things work, considering Lambda supports using container images, but I have nothing to prove that is how the Lambda service works.&lt;/p&gt;

&lt;p&gt;Containers are made up of different layers. If you've ever pulled down a container image from Docker Hub, GitHub, or elsewhere, you probably saw multiple progress bars. Each of those was for a layer of that image. Each layer is generally dependent on the layer before it, and when loaded on top of each other, they make the complete container image. Having multiple layers and the dependencies between them means you don't have to rebuild the entire container image if you make a change. Instead, only the layer you changed and any layers that depend on it need to be rebuilt.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc6d0sam0zifxz9drhjzd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc6d0sam0zifxz9drhjzd.png" alt="GenAI created alt text: A diagram of three blue layers, with the bottom-most layer labeled 'Base Image', the middle layer labeled 'Layer', and the top-most layer also labeled 'Layer'. The middle and top layers are connected by green arrows with the text 'Depends On' to the right." width="413" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Lambda OS-only runtime is effectively the base image for a container, which has the Lambda layers applied on top of it, with the source code you provide as the last layer applied.&lt;/p&gt;

&lt;p&gt;This is an oversimplification of container images, and not something you need to know if you want to use a custom runtime, but I've found this visualization useful when troubleshooting complex Lambda situations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building &amp;amp; Deploying the Bun Layer
&lt;/h2&gt;

&lt;p&gt;Bun's &lt;a href="https://github.com/oven-sh/bun/blob/main/packages/bun-lambda/README.md" rel="noopener noreferrer"&gt;readme for the bun-lambda package&lt;/a&gt; provides the commands necessary to build and deploy the Lambda layer to your AWS account. You need Bun and the AWS CLI installed and your terminal configured with access to the AWS account where you want the layer deployed.&lt;/p&gt;

&lt;p&gt;The commands default to building and deploying a layer for arm64 Lambdas and call the layer "bun". Since Lambda still defaults to the x86_64 architecture, I suggest a slightly different set of commands. The architecture for the layer does matter and determines which version of the bun runtime is downloaded, so I recommend deploying layers for both the x86_64 and arm64 architectures with a naming convention similar to the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/ps-integration-lambda-extensions.html" rel="noopener noreferrer"&gt;AWS Parameters and Secrets Lambda Extensions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Instead of running the publish layer script as detailed in the README, run the commands below. I ran into issues with the publish script correctly detecting the configured AWS region. I suggest manually entering the AWS region (or regions) you want the layer published to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bun run publish-layer &lt;span class="nt"&gt;--layer&lt;/span&gt; bun &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--arch&lt;/span&gt; x64 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 us-east-2
bun run publish-layer &lt;span class="nt"&gt;--layer&lt;/span&gt; bun-arm64 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--arch&lt;/span&gt; aarch64 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1 us-east-2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After running these commands, I had four Lambda layers in my AWS account, and the script output the ARNS for each of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arn:aws:lambda:us-east-1:000000000000:layer:bun:1
arn:aws:lambda:us-east-1:000000000000:layer:bun-arm64:1
arn:aws:lambda:us-east-2:000000000000:layer:bun:1
arn:aws:lambda:us-east-2:000000000000:layer:bun-arm64:1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating the Lambda Function
&lt;/h2&gt;

&lt;p&gt;The process of creating the Lambda function is mostly the same as you're used to, but with two important differences:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You have to add the bun or bun-arm64 Lambda layer to the Lambda function, depending on which architecture you chose for your Lambda&lt;/li&gt;
&lt;li&gt;You have to return a Response object from your function handler instead of a JavaScript object.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One of the quirks of the Bun runtime is the requirement to use the Response class as the return value from your handler functions unless you're working with websockets, which I'll get to in a moment.&lt;/p&gt;

&lt;p&gt;Below is a basic function handler that returns a JSON string with a message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from Lambda!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Bun Server Format
&lt;/h2&gt;

&lt;p&gt;The Bun runtime has built-in functionality for a &lt;a href="https://bun.sh/docs/api/http#bun-serve" rel="noopener noreferrer"&gt;high-performance web server&lt;/a&gt;, including WebSocket support. You can leverage this format to build your Lambda function, which lets you easily test your code locally without setting up containers or additional tools.&lt;/p&gt;

&lt;p&gt;Below is the code sample Bun provides for a Lambda that will respond to Amazon API Gateway requests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-amzn-function-arn&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Hello from Lambda!&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text/plain&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Bun custom Lambda runtime does not support the routes functionality of the Bun HTTP server added in Bun v1.2.3. It only supports the catch-all fetch function and the websocket function.&lt;/p&gt;

&lt;p&gt;I haven't dug into the WebSocket functionality; it seems like it is intended for use with API Gateway WebSocket APIs, but that isn't something I have had the occasion to use.&lt;/p&gt;

&lt;p&gt;It may seem like the Bun custom runtime is focused on supporting integrations with API Gateway, but it does support other event sources. This is an example of what a basic handler might look like for SQS, SNS, S3, EventBridge, and other services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Response&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing Locally
&lt;/h2&gt;

&lt;p&gt;If you use the Bun server format, you can run your handler function locally using Bun and your browser or an API testing tool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt; bun run handler.ts
Started development server: http://localhost:3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An HTTP server gets started on localhost, and you can use whatever tools you want to make requests to it. If you've ever used the Lambda testing capabilities of the SAM CLI, this is similar, except the Docker container used by the SAM CLI makes things a little more similar to the AWS environment.&lt;/p&gt;

&lt;p&gt;With the Bun server, it doesn't matter what kind of request you make, GET request, POST, or otherwise, it will execute the code. If you're really trying to mimic the AWS environment, you will need to put together the right HTTP body to make the request your function receives look like a request in AWS.&lt;/p&gt;

&lt;p&gt;It's handy to test something locally, but it's important to remember this isn't a full testing harness and isn't mocking over any AWS services. You either need to create those or use a tool like &lt;a href="https://www.localstack.cloud/" rel="noopener noreferrer"&gt;LocalStack&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is it Fast?
&lt;/h2&gt;

&lt;p&gt;In a very unscientific test, I created a Bun Lambda function that creates a JWT using the function ARN and the current ISO 8601 timestamp as the payload. It's not much computational work, but the Lambda function does everything and doesn't rely on outside systems. I invoked the function several times, waited a while, and repeated the process. This helped ensure I had a couple of cold starts to get initialization numbers.&lt;/p&gt;

&lt;p&gt;I had three cold starts in my dataset, and the initialization of the Lambda averaged 629.48ms. That initialization time is the extra time added by the cold start. Including the initialization time, the Lambdas averaged 75.66ms with 46 invocations. Removing that initialization time from consideration and focusing strictly on the handler's execution time, the average was 34.61ms.&lt;/p&gt;

&lt;p&gt;I adjusted the code to work in a normal Node.js 22 Lambda and followed the same procedure. The average execution time was 1.99ms, and the cold starts averaged 134.65ms. I know the Lambda team has a lot of optimizations in place for the base runtimes, and I don't know how much of this difference is due to those optimizations or to differences in Node and Bun.&lt;/p&gt;

&lt;p&gt;I may not be able to get super precise numbers, but I'm considering doing a more rigorous comparison between Node.js, Deno, and Bun Lambdas in the future.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping it Up
&lt;/h2&gt;

&lt;p&gt;Regardless of the numbers, the Bun Lambda is still fast, and with an easier way to test it locally, the trade-off may be worthwhile. Bun has excellent Node.js compatibility and has a lot of features that are great for the developer experience. I've used it for some automation scripts and doing local data transformations, and I'd be open to using it in production applications at this point, even in a Lambda.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>javascript</category>
      <category>serverless</category>
      <category>bunjs</category>
    </item>
    <item>
      <title>How Big Should Your Engineering Team Be?</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Thu, 17 Apr 2025 13:00:00 +0000</pubDate>
      <link>https://forem.com/jbutz/how-big-should-your-engineering-team-be-4b43</link>
      <guid>https://forem.com/jbutz/how-big-should-your-engineering-team-be-4b43</guid>
      <description>&lt;p&gt;Have you ever been on a team where it seemed like it took forever to get anything done? You had constant status meetings and could never seem to get a handle on what was being done. It's not an unusual situation, and it can have a variety of causes. How big was the team? Was it six or seven people? Maybe more? That could have been your problem.&lt;/p&gt;

&lt;p&gt;I was recently asked to propose a solution to reimagine an existing system. The system should use all the same data and database tables, but the rest of the application could be reimagined. The goal was to be able to deliver an MVP as soon as possible. I had to include the technical aspects and what the team of engineers should look like. The stakeholder wanted the MVP delivered in a month, maybe six weeks, and assured me that there were plenty of engineers available, so whatever it took to get the MVP delivered was fine. He told me he had 10 to 15 engineers he could assign to this easily if we could deliver the MVP in under 6 weeks.&lt;/p&gt;

&lt;p&gt;I proposed a team of five engineers, and he was surprised at how few I wanted.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mythical Man-Month
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;The Mythical Man-Month: Essays on Software Engineering&lt;/em&gt; by Fred Brooks was published in 1975 and contains many ideas and lessons that still apply today. The idea that I reference most, and which was at play in my recommendation for a small team, was the mythical man-month.&lt;/p&gt;

&lt;p&gt;There is an analogy or idiom I've heard that illustrates the mythical man-month concept well, even if it doesn't go into the reasoning from the book: "Nine women can't make a baby in a month." A pregnancy is about 9 months long, and the baby is growing and developing, which is not something you can divide among multiple people. The pregnancy will take roughly the same length of time, no matter what.&lt;/p&gt;

&lt;p&gt;Unlike a pregnancy, developing software can be divided between multiple people, but can't be perfectly divided into discrete, self-contained tasks without dependencies. There will be dependencies between tasks and the people doing the work. People will have to communicate and coordinate their work.&lt;br&gt;
Communication and task coordination are major limiting factors for development teams. When you have two people, there is only one line of communication between them. If you have a team of three, there are three lines of communication. If you have a team of 10 people, there are 45 lines of communication.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhqs6tfess28pz35ofrd8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhqs6tfess28pz35ofrd8.png" alt="Lines of communication between 2, 3, and 10 people. Graph charts showing lines of communication. Two people represented by circles, with one line between them. Three people represented by circles in a triangular formation with lines between them. Ten people represented by circles in a roughly circular formation with lines between all of them." width="664" height="823"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The formula for the lines of communication between people in a group is &lt;code&gt;n(n-1)/2&lt;/code&gt;, where &lt;code&gt;n&lt;/code&gt; is the number of people. If you really want to get into the math, this creates a polynomial growth curve.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvghc9odgqbnm0jb85cqo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvghc9odgqbnm0jb85cqo.png" alt="Growth curve of the number of connections within a group of people. Line graph with People on the Xaxis with 100 at the far right end and Connections on the Yaxis with 5000 at the top of the axis. Shows a second order polynomial growth curve" width="541" height="324"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Work Matters
&lt;/h2&gt;

&lt;p&gt;Regardless of the number of lines of communication, the kind of work you're doing matters when it comes to team size and scaling, and what can get done. When you can eliminate dependencies between people and tasks, you find opportunities to be more efficient.&lt;br&gt;
If your large team owns multiple microservices, for example, dividing the team between the microservices for the current sprint helps reduce the number of lines of communication. The same idea applies if you have 100 engineers migrating 20 microservices from Java to Go, for example. If you assign five people to each microservice, you have drastically reduced the number of lines of communication by removing dependencies outside those groups of five. In both these examples, you effectively created smaller teams.&lt;/p&gt;

&lt;p&gt;Suppose your team needs to create a lot of data models in your codebase, for example. In that case, you can often effectively split that across a large team if you properly plan for dependencies and relationships. The fewer dependencies and relationships, the easier the work will be with a large number of engineers.&lt;/p&gt;

&lt;p&gt;That's the key: Reducing the dependencies between work can enable you to leverage a larger team effectively. It's not always a great idea, but it's an option if you have no choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Can I Speed Up My Project with More People?
&lt;/h2&gt;

&lt;p&gt;Probably not. One of the key points in The Mythical Man-Month about the mythical man-month is what has been coined as Brooks' law: "Adding manpower to a late software project makes it later." Getting new people up to speed on the work will require coordination and communication, which will be affected by that growth curve from earlier.&lt;/p&gt;

&lt;p&gt;If the work is well enough defined, there are few enough dependencies, and the new people need very little context, you might be able to add people and speed up the project. But that is a lot of "maybes" and "ifs."&lt;/p&gt;

&lt;h2&gt;
  
  
  Small Teams Are Fast
&lt;/h2&gt;

&lt;p&gt;Several years ago, I led a team that fluctuated in size from four to seven engineers. When the team had seven engineers, it took a lot of mental effort to keep everything coordinated and moving forward; I felt like I was losing my mind. When we were down to four or five people, we had a few sprints where it was just me and one or two other engineers due to vacations and holidays. During a two-week sprint with four or five engineers, we completed 25 - 30 story points. When it was just me and two other engineers, we completed over 30 story points. Our last sprint that year was just me and one other engineer, and we completed just under 50 story points! This was the same kind of work we had been doing, but we reduced the necessary communication and coordination and produced amazing results! Sometimes, shrinking your teams is the answer.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/executive-insights/content/amazon-two-pizza-team/" rel="noopener noreferrer"&gt;Amazon&lt;/a&gt; and other companies are well known for using the "two pizza team" concept, the idea being that you should be able to feed the entire team with two large pizzas. According to this concept, Amazon says teams should be less than 10 people. I think teams need to be eight people or fewer, including leads, BAs, POs, or whatever. If someone is allocated 50% or more to the team, they count. For me, the sweet spot is around 4 to 5 engineers per team, including the lead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scaling Software Engineering is Different
&lt;/h2&gt;

&lt;p&gt;There are so many industries and disciplines where adding resources enables scaling. That's even the case with most systems we deploy, but not with our engineering teams. Many people think of software development as solitary work, like being a machine on an assembly line, but that's not the case. Software development is knowledge work, and there may be similar concepts and approaches across different projects and products. Still, they aren't the same, and there needs to be a lot of coordination.&lt;/p&gt;

&lt;p&gt;Keep your teams small, and communication and coordination are easier. Look for places to divide work and eliminate dependencies. Don't try to speed up a project by adding a bunch of developers to it; it won't work.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>leadership</category>
    </item>
    <item>
      <title>From PartyRock to Bedrock: AI-Powered Automation at Work</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Fri, 31 Jan 2025 17:00:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/from-partyrock-to-bedrock-ai-powered-automation-at-work-28c2</link>
      <guid>https://forem.com/aws-builders/from-partyrock-to-bedrock-ai-powered-automation-at-work-28c2</guid>
      <description>&lt;p&gt;There's been an explosion of tools claiming to make your job easier and touting GenAI, and I'm sure many of them can be very helpful. However, it can be hard to get support at many companies to integrate large new suites of tools into a process. It's often easier to add tools to your tasks. That's where the idea for this post came from.&lt;/p&gt;

&lt;p&gt;Last year, I told my boss and co-workers about &lt;a href="https://partyrock.aws" rel="noopener noreferrer"&gt;AWS's PartyRock tool&lt;/a&gt;. We'd been experimenting with tools like ChatGPT and HuggingChat and found some use cases, but we'd have to paste the prompt every time. It didn't make things easy to reuse and share. We had a lot of success with PartyRock, and now we're starting to use what was built with PartyRock to automate processes.&lt;/p&gt;

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

&lt;p&gt;PartyRock is a GenerativeAI-based tool that builds AI-powered applications using GenAI. The quickest way to get started is to write a prompt explaining what you want the application to do, and then PartyRock creates that application. There are some significant limitations to that. It won't turn your start-up idea into a working SaaS offering but can help with specific and limited tasks.&lt;/p&gt;

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

&lt;p&gt;Here are some application examples from PartyRock's site:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a meeting summary and list of action items when given meeting notes.&lt;/li&gt;
&lt;li&gt;Compare a job description and your resume and highlight gaps in your resume.&lt;/li&gt;
&lt;li&gt;Get grammar and writing feedback using PartyRock like an editor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A prompt for PartyRock will tend to be complex, but they have many examples to help get you started. Once you've generated your application, you can also fine-tune it by adjusting the prompts and other behaviors.&lt;/p&gt;

&lt;p&gt;PartyRock is currently free to use, but if you're going to use it for your job, you should probably disable sharing data with AWS. You should also review PartyRock's privacy policy, terms of service, and other legal stuff, along with your company's AI-use policies, to make sure what you're doing isn't going to cause problems, especially if you put anything sensitive into PartyRock.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F642j175nla1hlk0g3k1d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F642j175nla1hlk0g3k1d.png" alt="PartyRock screenshot, with an arrow pointing to " width="800" height="600"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Go to "Backstage" in PartyRock's menu to find the option to disable sharing data with AWS.&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  What we did with PartyRock
&lt;/h2&gt;

&lt;p&gt;We've tried out several different ideas with PartyRock, and I'm not going to go through all of them. Some didn't work out well, and some border on proprietary. One example I don't think anyone will mind my sharing is a tool to provide an initial review of RFPs.&lt;/p&gt;

&lt;p&gt;RFPs, or requests for proposals, are a big part of public-sector work. When working with governments and public institutions, like public schools, not public companies, there are generally rules about ensuring adequate competition between vendors that wish to provide services to the organization. That generally means documents are created that highlight the work that needs to be done, the expectations and requirements for the work, how the grading of a proposal works, and all kinds of other information. These documents are called RFPs and are what vendors use to create a proposal to try and win the work. Sometimes, these RFPs are a handful of pages long. Sometimes, they are hundreds of pages long. Reviewing these documents to see if they match your services can be a long and tedious process, and the format and quality of RFP documents are inconsistent. You can't just scroll to the page that says "services requested." Sometimes, that information is called out clearly. Sometimes, special codes indicate categories of services requested, and sometimes, you have to guess what the author was trying to request.&lt;/p&gt;

&lt;p&gt;Since these documents are generally required to be publicly accessible by law, there isn't too much concern about putting them into a GenAI tool, especially since we don't get RFPs for classified projects in the part of the company I work in. Only RFPs that are already accessible online.&lt;/p&gt;

&lt;p&gt;We used PartyRock to inspect these documents and determine the services being requested, how they match up against the services we offer, estimated budgets and timelines, and all kinds of other information that can quickly help us determine whether we want to read the entire RFP document. It's a huge timesaver! With PartyRock, we drop the PDF document onto the page, and PartyRock extracts and inspects the text. If we get a PDF from scanned documents, it doesn't work, but those RFPs aren't very common. Typically, everything is written on a computer, so extracting text from the PDF isn't hard.&lt;/p&gt;
&lt;h2&gt;
  
  
  Next Step - Bedrock APIs
&lt;/h2&gt;

&lt;p&gt;Having refined our prompts in PartyRock, we're now experimenting with integrating &lt;a href="https://aws.amazon.com/bedrock/" rel="noopener noreferrer"&gt;Amazon Bedrock&lt;/a&gt; into some of our tools and processes. We're a bunch of software engineers and use some open-source tools, so extending things with new functionality isn't a big step.&lt;/p&gt;

&lt;p&gt;Bedrock is AWS's serverless way to use different foundational models, without spinning up a bunch of infrastructure. We could do this all with SageMaker and other tools, or we can build on top of Bedrock. Which is what PartyRock does.&lt;/p&gt;

&lt;p&gt;We were a little concerned that working with documents and Bedrock was going to mean a bunch of effort by using &lt;a href="https://aws.amazon.com/textract/" rel="noopener noreferrer"&gt;Texttract&lt;/a&gt;. I was glad we were proven wrong. I was able to build a quick proof of concept using the Bedrock API in 10 - 15 minutes.&lt;/p&gt;

&lt;p&gt;Using a very basic prompt and a small sample document I wrote myself, I was able to call Bedrock's API and get a response from Amazon's Nova Lite model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;BedrockRuntimeClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;ConverseCommand&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@aws-sdk/client-bedrock-runtime&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sampleDocument&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sample.pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BedrockRuntimeClient&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;us-east-1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;client&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ConverseCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;amazon.nova-lite-v1:0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;document&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rfp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pdf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                  &lt;span class="na"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sampleDocument&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
              &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
              &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Review the RFP, and provide a summary of the services being requested&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
          &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once I knew I could get the kind of output I needed, I started working on an API to allow us to easily call Bedrock on the format we needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  API Gateway &amp;amp; Bedrock
&lt;/h2&gt;

&lt;p&gt;I initially looked at directly integrating Amazon API Gateway with the Bedrock API through an AWS service integration. I'm a big supporter of avoiding unnecessary Lambda functions and having API Gateway directly call services whenever possible. AWS Hero Yan Cui recently &lt;a href="https://www.linkedin.com/posts/theburningmonk_stop-using-lambda-functions-when-you-dont-activity-7281232909372416000-JoCF/" rel="noopener noreferrer"&gt;posted on LinkedIn&lt;/a&gt; and uploaded a &lt;a href="https://www.youtube.com/watch?v=_NnO2JiPTEw" rel="noopener noreferrer"&gt;YouTube video&lt;/a&gt; discussing the same thing. Unfortunately, API Gateway doesn't currently support Bedrock. At least, not easily. Hopefully that'll be added soon.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 If you haven't used AWS service integrations with API Gateway, you should &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-aws-proxy.html" rel="noopener noreferrer"&gt;take a look at this tutorial from AWS&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I created a Lambda function that handles the API call to Bedrock and had API Gateway invoke the Lambda function. Altogether, it took a couple of hours to build an API that is good enough to start adding GenAI into our processes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Road Ahead
&lt;/h2&gt;

&lt;p&gt;We still need to use this API, but being able to use Bedrock so easily seems like a game changer. PartyRock was great when just a few of us were trying something out, but now that we have a use case worth being scaled out, it's time to use Bedrock directly and remove as much of the manual process as possible.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>genai</category>
      <category>aws</category>
    </item>
    <item>
      <title>How to Enable Multi-Session Support in the AWS Console</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Thu, 16 Jan 2025 20:21:08 +0000</pubDate>
      <link>https://forem.com/aws-builders/how-to-enable-multi-session-support-in-the-aws-console-2han</link>
      <guid>https://forem.com/aws-builders/how-to-enable-multi-session-support-in-the-aws-console-2han</guid>
      <description>&lt;p&gt;AWS has finally &lt;a href="https://aws.amazon.com/about-aws/whats-new/2025/01/aws-management-console-simultaneous-sign-in-multiple-accounts/" rel="noopener noreferrer"&gt;released support for users to sign in to multiple AWS accounts simultaneously&lt;/a&gt;, without extra browsers or using special browser features. Multi-session support has been a missing capability for a long time, and enabling it is a breeze.&lt;/p&gt;

&lt;p&gt;Open the account menu in the console's upper-right corner and click the &lt;em&gt;Turn on multi-session&lt;/em&gt; button. You'll get a warning telling you the console URL will be different; click on the &lt;em&gt;Turn on multi-session&lt;/em&gt; button if that isn't a concern. The page will reload, and the console URL will have your account number in it .&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmhon2excm70qhcyeg6fh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmhon2excm70qhcyeg6fh.png" alt="AWS Console showing the Turn on multi-session button in the account menu" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, you click &lt;em&gt;Add session&lt;/em&gt; to open a window to log into another AWS account. If you use the IAM Identity Center (AWS SSO ), or some other SSO mechanism, you can open another account through that as well. I tested this with IAM Identity Center, and each account I opened used the new multi-session URL for the console. Multi-session support is a per-browser setting and not a per-account setting. Clearing your cookies will turn off the feature.&lt;/p&gt;

&lt;p&gt;Multi-session support only supports 5 simultaneous sessions; if you open a 6th, you get a screen asking which session you want to log out of.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2oxup8w4b0nz9vbozqw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff2oxup8w4b0nz9vbozqw.png" alt="AWS Multi-Session session selection screen when you try and open a 6th session" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Multi-session support has been a long-awaited feature, and it's great to see it in the console. It's a more convenient alternative to using Firefox Containers, Chrome and Edge Profiles, or private browsing windows. Everything works very well and provides a smooth experience. I hope the limit of 5 accounts is adjusted or removed, but maybe having more than five accounts open isn't common for most people. How many AWS accounts do you typically need simultaneous access to? Tell me in the comments.&lt;/p&gt;

</description>
      <category>aws</category>
    </item>
    <item>
      <title>Chaos Engineering with AWS FIS and Lambda</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Wed, 20 Nov 2024 20:30:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/chaos-engineering-with-aws-fis-and-lambda-2b7o</link>
      <guid>https://forem.com/aws-builders/chaos-engineering-with-aws-fis-and-lambda-2b7o</guid>
      <description>&lt;p&gt;Recently &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/10/aws-lambda-fault-injection-service-actions/" rel="noopener noreferrer"&gt;AWS's Fauly Injection Service (FIS) added support for AWS Lambda&lt;/a&gt;, maybe it's the other way around, but either way, they now work together. I'd never given FIS much focus; most organizations I work with aren't ready for or interested in chaos engineering. However, the more I looked into FIS, the more I realized I had misjudged what FIS was capable of and where I could use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Chaos Engineering?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"Chaos Engineering is the discipline of experimenting on a system in order to build confidence in the system’s capability to withstand turbulent conditions in production."&lt;br&gt;
— &lt;a href="https://principlesofchaos.org/" rel="noopener noreferrer"&gt;Principles of Chao Engineering&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I first heard about chaos engineering due to Netflix and their &lt;a href="https://github.com/netflix/chaosmonkey" rel="noopener noreferrer"&gt;Chaos Monkey tool&lt;/a&gt;, which later evolved into the now retired &lt;a href="https://github.com/Netflix/SimianArmy" rel="noopener noreferrer"&gt;Simian Army&lt;/a&gt; and has since been split out again. I've actually worked in an environment where Chao Monkey was running, and it does influence how you build applications, but once you lay your foundations and patterns, it's not too bad.&lt;/p&gt;

&lt;p&gt;AWS primarily identifies FIS as a resiliency-focused tool, but it uses chaos engineering principles at its heart. You're impacting the performance of your system through an experiment to see how things behave to different failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Introduction to AWS FIS
&lt;/h2&gt;

&lt;p&gt;AWS FIS enables you to run experiments on certain AWS resources to test how your system responds to different fault conditions. FIS has a limited selection of actions that can be performed against different targets, i.e., AWS resources. At the time of writing, FIS can target &lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/targets.html#resource-types" rel="noopener noreferrer"&gt;18 different types of resources&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aurora DB clusters&lt;/li&gt;
&lt;li&gt;RDS DB instances&lt;/li&gt;
&lt;li&gt;DynamoDB global tables&lt;/li&gt;
&lt;li&gt;EBS volumes&lt;/li&gt;
&lt;li&gt;EC2 Auto Scaling groups&lt;/li&gt;
&lt;li&gt;EC2 instances&lt;/li&gt;
&lt;li&gt;EC2 Spot Instances&lt;/li&gt;
&lt;li&gt;ECS clusters&lt;/li&gt;
&lt;li&gt;ECS tasks&lt;/li&gt;
&lt;li&gt;EKS clusters&lt;/li&gt;
&lt;li&gt;EKS node groups&lt;/li&gt;
&lt;li&gt;EKS Kubernetes pods&lt;/li&gt;
&lt;li&gt;S3 buckets&lt;/li&gt;
&lt;li&gt;VPC subnets&lt;/li&gt;
&lt;li&gt;Lambda functions&lt;/li&gt;
&lt;li&gt;ElastiCache (Redis OSS) Replication Groups&lt;/li&gt;
&lt;li&gt;IAM roles&lt;/li&gt;
&lt;li&gt;transit gateways&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In a FIS experiment, you combine actions and targets, generally running them for a specific duration. You can add additional parameters that help focus your experiment, such as stopping only 1% of EC2 instances.&lt;/p&gt;

&lt;p&gt;During and after your experiment, you can inspect your logs and other metrics to see how the system performed. FIS offers a feature to generate a report that collects CloudWatch metrics and combines them with experiment details into a PDF report. I've looked at the &lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/experiment-report-configuration.html" rel="noopener noreferrer"&gt;example report&lt;/a&gt;, and I'm not sure it's worth the $5 cost to generate the report.&lt;/p&gt;

&lt;h2&gt;
  
  
  FIS and Lambda
&lt;/h2&gt;

&lt;p&gt;With AWS's recent release, there are three actions available that can target Lambda functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Invocation delay (&lt;code&gt;invocation-add-delay&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Invocation error (&lt;code&gt;invocation-error&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Invocation HTTP integration response (&lt;code&gt;invocation-http-integration-response&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Adding an invocation delay is what it sounds like. It's similar to a Lambda cold start, but the delay added by FIS is after any cold starts. In addition to simulating a cold start, you can create timeout events by configuring the added latency higher than the Lambda function's timeout.&lt;/p&gt;

&lt;p&gt;The invocation error action allows you to mark function invocations as failed. That will be helpful for testing error handling and retry mechanisms. Interestingly, you can also decide if you want to allow the Lambda function to execute its handler. That could be useful if you want to test whether certain operations are actually &lt;a href="https://en.wikipedia.org/wiki/Idempotence" rel="noopener noreferrer"&gt;idempotent&lt;/a&gt;, i.e., performing the same action multiple times has no effect after the first time the action was performed. It might also be a way to avoid interrupting known idempotent operations and allow them to process events despite the experiment.&lt;/p&gt;

&lt;p&gt;The HTTP integration response action is intended to work with Application Load Balancers (ALBs), API Gateways, and VPC Lattice. You can select a content type and HTTP response code that are returned. You cannot set the response body, which I find very disappointing. You can also decide whether to allow the Lambda function to execute its handler with this action. Again, this could test idempotency or limit the disruption caused by the experiment.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does FIS work with Lambda?
&lt;/h3&gt;

&lt;p&gt;The Lambda functions targeted by your FIS experiments must have the &lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/actions-lambda-extension-arns.html" rel="noopener noreferrer"&gt;FIS Lambda Layer&lt;/a&gt; added. This is central to how FIS can perform different actions. You also need an S3 bucket to store the experiment configuration.&lt;/p&gt;

&lt;p&gt;When the FIS experiment is initializing, FIS uses the service role you define in the experiment template to write configuration files to the S3 bucket at a well-known path. The FIS Lambda Layer, using your Lambda function's execution role, checks that well-known path in the configured S3 bucket at a configured frequency. Those checks from your Lambda function to the S3 bucket happen regardless of any FIS experiments, so they are increasing the number of S3 API calls and, presumably, the billed duration for your Lambda functions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe31wtcjchmgn179uki31.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe31wtcjchmgn179uki31.png" alt="Lambda service and FIS service interact with the defined S3 bucket to read and write configurations" width="800" height="184"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The FIS Lambda Layer uses the &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/runtimes-api.html" rel="noopener noreferrer"&gt;AWS Lambda Runtime API&lt;/a&gt; proxy to intercept function invocations. This is before the invocation reaches the runtime, which makes the FIS layer runtime agnostic. As part of configuring your Lambda functions for FIS, you set the environment variable &lt;code&gt;AWS_LAMBDA_EXEC_WRAPPER&lt;/code&gt; to &lt;code&gt;/opt/aws-fis/bootstrap&lt;/code&gt;. This is what enables that Lambda Runtime API proxy. FIS uses some of the Lambda runtime environment modification capabilities to provide functionality. You don't need to worry too much about the details unless you are using additional extensions to the Lambda environment, in which case &lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/use-lambda-actions.html" rel="noopener noreferrer"&gt;you'll need to set up a proxy chain&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fngwq6pzlvy2xqzqv2vvb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fngwq6pzlvy2xqzqv2vvb.png" alt="Diagram showing the FIS Lambda extension wrapping the Lambda runtime and proxying calls to the Lambda runtime API" width="800" height="774"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting up a Lambda function for FIS
&lt;/h3&gt;

&lt;p&gt;As mentioned earlier, you must have an Amazon S3 bucket to use with the FIS experiment. It must be in the same region as the experiment you plan to run. You must also update your Lambda execution role with a policy to allow access to the S3 bucket as specific prefixes. You will also need an IAM policy on the role associated with your FIS experiment that grants access to the bucket, allows FIS to inspect Lambda functions, and enables FIS to do tag-based lookups. Details on what these IAM policies should look like are in the &lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/use-lambda-actions.html#lambda-prerequisites" rel="noopener noreferrer"&gt;FIS documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Once you have all of that, you need to make a few minor modifications to your Lambda functions that will be involved in the experiment. First, you should add the FIS Lambda layer, details and ARNs for the layers in different regions are in the &lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/actions-lambda-extension-arns.html" rel="noopener noreferrer"&gt;AWS documentation&lt;/a&gt;. Then you should add two environment variables to the functions. The first is &lt;code&gt;AWS_FIS_CONFIGURATION_LOCATION&lt;/code&gt; with an S3 bucket ARN that points to the &lt;code&gt;FisConfigs&lt;/code&gt; prefix in the S3 bucket you set up, for example &lt;code&gt;arn:aws:s3:::my-config-distribution-bucket/FisConfigs/&lt;/code&gt;. This lets the FIS layer know where it should look for configuration details. The second is &lt;code&gt;AWS_LAMBDA_EXEC_WRAPPER&lt;/code&gt; with &lt;code&gt;/opt/aws-fis/bootstrap&lt;/code&gt; as the value. This sets up the Lambda Runtime API proxy mentioned earlier.&lt;/p&gt;

&lt;p&gt;This configuration is only the most basic configuration; depending on your functions, additional considerations may exist. Some of these considerations are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Short experiment action durations&lt;/li&gt;
&lt;li&gt;Using SnapStart&lt;/li&gt;
&lt;li&gt;Fast and infrequently invoked functions&lt;/li&gt;
&lt;li&gt;Functions already using Lambda extensions&lt;/li&gt;
&lt;li&gt;Functions using container runtimes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://docs.aws.amazon.com/fis/latest/userguide/use-lambda-actions.html" rel="noopener noreferrer"&gt;AWS FIS documentation outlines what you need to know&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How about an example?
&lt;/h2&gt;

&lt;p&gt;These details are great, but how about an example showing what you can do with FIS and Lambda?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgp3v1x91nlgvqvrhkc0c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgp3v1x91nlgvqvrhkc0c.png" alt="AWS architecture sketch, showing an API Gateway with an arrow pointing to an SQS queue that has arrows pointing to both a Lambda function and an SQS DLQ. The SQS DLQ is connected to an EventBridge pipe" width="800" height="448"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's take the architecture sketched out above. Our Lambda function is invoked with messages from the SQS queue, but if the messages fail to process three times, they are sent to the DLQ and onto an EventBridge pipe, which leads to logic not relevant to this example. In this example, we follow AWS best practices and have our visibility timeout on the SQS queue set to six times the Lambda timeout. With a Lambda timeout of 10 seconds, our visibility timeout is 60 seconds.&lt;/p&gt;

&lt;p&gt;For this example, our EventBridge pipe and the associated error-handling system are new and need testing. We've already tested our logic by sending messages directly to the DLQ, but now we need to test the entire thing. We've decided to use FIS to simulate the Lambda function taking too long to process messages. We've also decided to run this experiment in our development environment. We're lucky and can break the processing of these messages for a short period of time.&lt;/p&gt;

&lt;p&gt;In our FIS experiment template, we'll configure an &lt;code&gt;aws:lambda:invocation-add-delay&lt;/code&gt; action with a startup delay of 30,000 milliseconds (30 seconds). This will ensure our Lambda function invocations time out. Since we can break the processing of these messages, we can set our action to run 100% of the time. We need to do a little math to ensure we keep our experiment running long enough.&lt;/p&gt;

&lt;p&gt;Once our experiment is running, our Lambda function will be invoked (and time out) three times for every message added to the queue. Between those invocations, the message will be delayed for at least the length of time of our visibility timeout. Because our Lambda function will be timing out, its duration doesn't matter. The SQS queue's visibility timeout is the important duration here.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="katex-element"&gt;
  &lt;span class="katex-display"&gt;&lt;span class="katex"&gt;&lt;span class="katex-mathml"&gt;60 seconds×3 delivery attempts=180 seconds
60\ seconds \times 3\  delivery\ attempts = 180\  seconds
&lt;/span&gt;&lt;span class="katex-html"&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;60&lt;/span&gt;&lt;span class="mspace"&gt; &lt;/span&gt;&lt;span class="mord mathnormal"&gt;seco&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mbin"&gt;×&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;3&lt;/span&gt;&lt;span class="mspace"&gt; &lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;l&lt;/span&gt;&lt;span class="mord mathnormal"&gt;i&lt;/span&gt;&lt;span class="mord mathnormal"&gt;v&lt;/span&gt;&lt;span class="mord mathnormal"&gt;ery&lt;/span&gt;&lt;span class="mspace"&gt; &lt;/span&gt;&lt;span class="mord mathnormal"&gt;a&lt;/span&gt;&lt;span class="mord mathnormal"&gt;tt&lt;/span&gt;&lt;span class="mord mathnormal"&gt;e&lt;/span&gt;&lt;span class="mord mathnormal"&gt;m&lt;/span&gt;&lt;span class="mord mathnormal"&gt;pt&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;span class="mrel"&gt;=&lt;/span&gt;&lt;span class="mspace"&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="base"&gt;&lt;span class="strut"&gt;&lt;/span&gt;&lt;span class="mord"&gt;180&lt;/span&gt;&lt;span class="mspace"&gt; &lt;/span&gt;&lt;span class="mord mathnormal"&gt;seco&lt;/span&gt;&lt;span class="mord mathnormal"&gt;n&lt;/span&gt;&lt;span class="mord mathnormal"&gt;d&lt;/span&gt;&lt;span class="mord mathnormal"&gt;s&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;/div&gt;


&lt;p&gt;It will take &lt;em&gt;at least&lt;/em&gt; 180 seconds before each message is added to the DLQ. Assuming we can cause messages to be added to the queue quickly, we should be able to get multiple messages to the DLQ within a 5-minute duration for this action. That gives us a little wiggle room. If getting messages into the queue takes longer, we will want a longer duration, for example, 10 minutes. We should get some information if the duration is at least 3 minutes. Less than 3 minutes or 3 minutes exactly, and we might not receive messages to our DLQ. There can be a delay of up to a minute before the Lambda function follows the actions outlined in our experiment, and the visibility timeout on the SQS is a minimum duration of time before the message is placed back in the queue. It does not mean it will be immediately reprocessed at that time.&lt;/p&gt;

&lt;p&gt;We should be able to test this path in our application with all these configurations, setting up our targets for the experiment and making the changes needed to our Lambda function.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Digging into AWS FIS's Lambda support expanded my understanding of chaos engineering. It helped me see far more possibilities for where it can be used than I had ever thought about. Chaos engineering is much more than terminating instances; with carefully planned experiments, you can test exactly how your system responds to a variety of issues. I hope the FIS team will continue to expand support for additional functionality with Lambda and other AWS services.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>chaosengineering</category>
      <category>cloud</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Lit - Lighting Fast Web Components</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Fri, 11 Oct 2024 13:00:00 +0000</pubDate>
      <link>https://forem.com/jbutz/lit-lighting-fast-web-components-345l</link>
      <guid>https://forem.com/jbutz/lit-lighting-fast-web-components-345l</guid>
      <description>&lt;p&gt;Ever since the first release of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_components" rel="noopener noreferrer"&gt;web components&lt;/a&gt; I've been interested in them. A way to build components that are isolated from each other without needing a bulky library or framework sounded amazing. Unfortunately, they tend to be hard to work with and browser support was slow to come.&lt;/p&gt;

&lt;p&gt;Lit is a small library, built on top of web components, that makes it much easier to build interoperable web components. The team released &lt;a href="https://lit.dev/" rel="noopener noreferrer"&gt;Lit version 3&lt;/a&gt; last year, and I just got around to trying it out. I'm impressed with what Lit is capable of. There are a few things missing that I'd want to see before building out a major application, like a router API, but it seems like a great fit if you need to build some web components to drop into an existing site. It might even be nice for a framework agnostic design system.&lt;/p&gt;

&lt;p&gt;Let's take a look at a simple project. In this case I'll make a very simple to-do list application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start the Project
&lt;/h2&gt;

&lt;p&gt;Lately, when it comes to starting a new frontend project &lt;a href="https://vite.dev/" rel="noopener noreferrer"&gt;Vite&lt;/a&gt; has been my go-to tool. They have starting templates that fit for most of the libraries I would use, and it "just works".&lt;/p&gt;

&lt;p&gt;To start out we need to generate our project, I'm choosing the Lit TypeScript template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm create vite@latest my-lit-app &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--template&lt;/span&gt; lit-ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should give you a directory tree similar to what I have below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-lit-app
├── public
│   └── vite.svg
├── src
│   ├── my-element.ts
│   ├── index.css
│   ├── vite-env.d.ts
│   └── assets
│       └── lit.svg
├── .gitignore
├── index.html
├── package.json
└── tsconfig.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there you will need to install your NPM dependencies, then we can start the application to see what we have.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install NPM dependencies&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="c"&gt;# Start the application in development mode&lt;/span&gt;
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you open the URL provided, this is what your page should look like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv639y9r8cyn1mm67umc0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv639y9r8cyn1mm67umc0.png" alt="Screenshot of the default Vite Lit template page, showing the Vite lightning bolt logo and Lit fire polygon logo above 'Vite + Lit' with a button below that increments a counter when clicked" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Component
&lt;/h2&gt;

&lt;p&gt;To keep things simple, let's replace the component the template provided with one of our own. I've replaced the contents of &lt;code&gt;src/my-element.ts&lt;/code&gt; with what I have below. This is a very simple boilerplate that will still build and render successfully, serving as a foundation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LitElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;customElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;property&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit/decorators.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;customElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-element&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyElement&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="s2"&gt;``&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
      &amp;lt;div&amp;gt;&amp;lt;/div&amp;gt;
    `&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;HTMLElementTagNameMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-element&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MyElement&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expanding from here, I want to get my HTML elements onto the page. After all, that's what is going to let us see progress and start adding functionality. Inside the &lt;code&gt;render&lt;/code&gt; method you'll notice a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals" rel="noopener noreferrer"&gt;template literal&lt;/a&gt;, specifically a &lt;a href="https://lit.dev/docs/templates/overview/" rel="noopener noreferrer"&gt;tagged template named &lt;code&gt;html&lt;/code&gt;&lt;/a&gt;, is being used to return the HTML the component will render. I'll add a form with a text input field and a button, along with an empty unordered list element. This special tagged template is part of what Lit provides to make building web components easier. It's important to note, this isn't JSX. It's HTML. You don't have the same restrictions, like needing a single parent element.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
    &amp;lt;form&amp;gt;
      &amp;lt;input type="text" name="todoItem" /&amp;gt;
      &amp;lt;button&amp;gt;Add Item&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
    &amp;lt;ul&amp;gt;&amp;lt;/ul&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, we need a place to put the items for our to-do list and a way to display them on the page. To do that we will use the &lt;a href="https://lit.dev/docs/api/decorators/#state" rel="noopener noreferrer"&gt;&lt;code&gt;@state&lt;/code&gt; directive&lt;/a&gt;. This directive lets us add a private or protected property that still triggers updates to the component. Our array is going to contain objects, so one thing we need to do is help Lit with determining if our value has changed. This is because a new object, even if it has the exact same properties, will not be considered equal when using the &lt;code&gt;===&lt;/code&gt; operator. One easy way to make this comparison is to convert our values to JSON strings, and then compare them. That's what I have done below with the &lt;code&gt;hasChanged&lt;/code&gt; function.&lt;/p&gt;

&lt;p&gt;This code can be added at the top of our class near the &lt;code&gt;style&lt;/code&gt; property.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;state&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;hasChanged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="nx"&gt;oldValue&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Item 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Item 2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that we have items for our list, we need to update our HTML to display them. Lit has something to help make sure we keep DOM updates to the minimum, it's called the &lt;a href="https://lit.dev/docs/templates/lists/#the-repeat-directive" rel="noopener noreferrer"&gt;&lt;code&gt;repeat&lt;/code&gt; directive&lt;/a&gt;. We pass our array of items, a function that returns an item's id, and a function that renders an item to the directive and it returns the HTML for our list. When our array of items changes, it will help minimize the number of updates. This behavior is something you have to purposefully use, but it's there for the same reason the &lt;code&gt;key&lt;/code&gt; attribute is in JSX/React. You may notice the odd &lt;code&gt;?checked=${...}&lt;/code&gt; notation below. That is how Lit handles boolean attributes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
    &amp;lt;form&amp;gt;
      &amp;lt;input type="text" name="todoItem" /&amp;gt;
      &amp;lt;button&amp;gt;Add Item&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
    &amp;lt;ul&amp;gt;
      &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;li&amp;gt;
            &amp;lt;input type="checkbox" ?checked=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; /&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
          &amp;lt;/li&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
    &amp;lt;/ul&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can see our list now, but we can't add new items and we aren't saving the checked state of our items.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj4dv1zhe9l63ylmc2fur.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj4dv1zhe9l63ylmc2fur.png" alt="Screenshot showing a text input field with a button labeled 'Add Item' beside it. Below is a list with items labeled 'Item 1' and 'Item 2' and a checkbox beside each item" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it Interactive
&lt;/h2&gt;

&lt;p&gt;Let's start with adding new items to our to-do list. First, use the &lt;a href="https://lit.dev/docs/api/decorators/#query" rel="noopener noreferrer"&gt;&lt;code&gt;@query&lt;/code&gt; decorator&lt;/a&gt; to make it easy to access our text input element. We pass the decorator the CSS selector for the element we want to find. Then we are able to access the element through the variable the decorator is applied to. That will make it easy for us to get the input element's value. Next, we'll need to create a private handler method to update our to-do list with the new item. Similar to how you would do it with React, I am defining a new array and assigning it to the variable. To create a unique ID value I am getting the current unix epoch and adding a random number. The epoch number is probably good enough, but a little randomness isn't going to hurt. At the end, after adding the item to the list, I am clearing the value of the text input and setting focus to the input element. &lt;br&gt;
You might notice the &lt;code&gt;e.preventDefault();&lt;/code&gt; line. I'm using the &lt;code&gt;submit&lt;/code&gt; event here, and that line will prevent the default behavior when submitting a form. That is, to make an HTTP request to the page defined in the &lt;code&gt;action&lt;/code&gt; property, or the current page if that is undefined. We don't need that behavior so do disabling it is the way to do. We could avoid it with a click event, but by using the submit event a user typing something in and pressing enter works exactly as you'd expect with no additional effort.&lt;br&gt;
On the HTML form tag you can see a new attribute that attaches the submit event listener to our form. The &lt;code&gt;@submit&lt;/code&gt; syntax is how you attach the listener to a given element. Next, we'll be attaching a click event for our checkboxes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[name="todoItem"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;addItemInputEl&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;_handleAddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubmitEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addItemInputEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addItemInputEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addItemInputEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
 &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
   &amp;lt;form @submit=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_handleAddItem&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;
   // ...
 `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
 &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We'll need to create a private handler method for the click event we'll be generating. We'll take advantage of &lt;a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Event_bubbling" rel="noopener noreferrer"&gt;event bubbling&lt;/a&gt; so that clicking on the checkbox and the to-do item itself both cause our checkbox to be checked. To help with identifying the correct item, we'll add a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*" rel="noopener noreferrer"&gt;data attribute&lt;/a&gt; that contains our item's ID. Our handler needs to determine the item ID to search for and update. The &lt;code&gt;currentTarget&lt;/code&gt; property of the event will contain a reference to our list item element, since that is where the event handler is attached. From there, it's a matter of creating a new array with the modified to-do item with the &lt;code&gt;complete&lt;/code&gt; value set to the opposite of what it is currently. This ensures we can both check and uncheck an item by clicking on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;_handleItemClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PointerEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTarget&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
    // ...
        html`&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;${id}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;click&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_handleItemClick&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;checkbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="nx"&gt;checked&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nx"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="sr"&gt;/&amp;gt; ${value&lt;/span&gt;&lt;span class="err"&gt;}
&lt;/span&gt;          &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/li&amp;gt;`&lt;/span&gt;&lt;span class="err"&gt;,
&lt;/span&gt;    &lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="s2"&gt;`;
}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our to-do list is working now, but it's not obvious you can click on an item to toggle the checkbox. With just a little CSS we can change that. Update the string inside the &lt;code&gt;css&lt;/code&gt; tagged template and then your mouse cursor will change to a pointer anytime you are hovering over one of the to-do items.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="s2"&gt;`
  ul li {
    cursor: pointer;
  }
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One of the nice things about web components and Lit, is this CSS is scoped to our component. So it won't affect anything else on our page. With a few more CSS changes we can have this to-do list looking even better.&lt;/p&gt;

&lt;p&gt;Below is the full component. With some additional CSS changes to the &lt;code&gt;index.css&lt;/code&gt; file we can have the application looking even better. Our to-do list disappears if you refresh the page, but I'm not going to go into what it would take to save and restore that from &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage" rel="noopener noreferrer"&gt;&lt;code&gt;localStorage&lt;/code&gt;&lt;/a&gt;, that is an exercise I'll leave up to you.&lt;/p&gt;

&lt;p&gt;Lit is a fast way to develop web components. This example results in a JS bundle that is about 22kB, which could be smaller but also isn't horrible. Lit has good documentation, and makes it easy to get started. There are still some features I'd like to see, but a lot of them are already in the work. What do you think? Do you like Lit? Do you want me to create the same application in vanilla web components to see what the difference in difficulty and bundle size really is? Let me know on social media.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;LitElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;customElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit/decorators.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;repeat&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;lit/directives/repeat.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;customElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-element&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;MyElement&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;LitElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;css&lt;/span&gt;&lt;span class="s2"&gt;`
    ul {
      list-style: none;
      padding-left: 0;
    }
    ul li {
      cursor: pointer;
    }
    ul li input[type='checkbox'] {
      margin-right: 0.5em;
    }
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;state&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;hasChanged&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;?,&lt;/span&gt; &lt;span class="nx"&gt;oldValue&lt;/span&gt;&lt;span class="p"&gt;?)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oldValue&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Item 1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Item 2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="p"&gt;@&lt;/span&gt;&lt;span class="nd"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[name="todoItem"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;addItemInputEl&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;_handleAddItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SubmitEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;preventDefault&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addItemInputEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addItemInputEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addItemInputEl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;focus&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;_handleItemClick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="na"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PointerEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;el&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currentTarget&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`
    &amp;lt;form @submit=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_handleAddItem&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;
      &amp;lt;input type="text" name="todoItem" /&amp;gt;
      &amp;lt;button&amp;gt;Add Item&amp;lt;/button&amp;gt;
    &amp;lt;/form&amp;gt;
    &amp;lt;ul&amp;gt;
      &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;todoItems&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;li data-id="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" @click=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_handleItemClick&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;
            &amp;lt;input type="checkbox" ?checked=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; /&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;
          &amp;lt;/li&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
    &amp;lt;/ul&amp;gt;
  `&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;HTMLElementTagNameMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;my-element&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;MyElement&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>webdev</category>
      <category>webcomponents</category>
      <category>lit</category>
    </item>
    <item>
      <title>AWS Certified AI Practitioner Beta Exam Reaction</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Sat, 21 Sep 2024 19:26:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/aws-certified-ai-practitioner-beta-exam-reaction-4bi0</link>
      <guid>https://forem.com/aws-builders/aws-certified-ai-practitioner-beta-exam-reaction-4bi0</guid>
      <description>&lt;p&gt;The other day, I took and passed the new AWS Certified AI Practitioner beta exam. I was surprised at how difficult and technical it was for a foundational exam. To be completely clear, AI/ML is not my focus, and I have limited experience with it. I've used GenAI and other ML models but with no tuning. I know about many concepts like knowledge bases, agents, and embeddings, and I have some understanding of how they work. I know about some prompt engineering techniques but haven't studied them intensely. Machine learning wasn't a focus during my undergraduate studies, but I did learn a little about machine learning and how some of it works. Generative AI wasn't even a concept back then. I passed the exam with a score of 704; I needed a 700 to pass.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.credly.com/earner/earned/badge/919495d4-8bd1-44c5-bcc6-3db6f6b5e455" rel="noopener noreferrer"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwhsm3iotlm63dzr1gsu5.png" alt="AWS Certified AI Practitioner Early Adopter" width="340" height="340"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I should start by thanking AWS and the training and certification team. Through messages I received as an AWS Community Builder, I was able to apply for a voucher to take the beta exam for free. I think the goal was to get people with the correct skill set for the exam to take it and ensure the team behind the exam gets good data to move the exam from a beta version to a final version. I qualified for and received one of those vouchers, so I took the exam at no financial cost to myself.&lt;/p&gt;

&lt;p&gt;I did not study for this exam. Part of that was because I believed I had the knowledge for it, considering it was a foundational exam, and I exceeded the target audience based on the &lt;a href="https://aws.amazon.com/certification/certified-ai-practitioner/" rel="noopener noreferrer"&gt;intended candidate and the candidate role examples on the AWS website&lt;/a&gt; . I may have overestimated my knowledge because I barely passed, or the exam may not target the desired audience as well as it could. Maybe a little of both.&lt;/p&gt;

&lt;p&gt;The AWS website says the intended candidates are "Individuals who are familiar with, but do not necessarily build, solutions using AI/ML technologies on AWS." They list "business analyst, IT support, marketing professional, product or project manager, line-of-business or IT manager, sales professional" as example candidate roles. I would not expect candidates in those roles to pass this exam without intensive study.&lt;/p&gt;

&lt;p&gt;I can't tell you the questions that cause me to think that. I must comply with &lt;a href="https://aws.amazon.com/certification/certification-agreement/" rel="noopener noreferrer"&gt;AWS's certification agreement&lt;/a&gt; , so I can't discuss the exam's questions. But I can talk about what is in the &lt;a href="https://d1.awsstatic.com/training-and-certification/docs-ai-practitioner/AWS-Certified-AI-Practitioner_Exam-Guide.pdf" rel="noopener noreferrer"&gt;exam guide&lt;/a&gt; on &lt;a href="https://aws.amazon.com/certification/certified-ai-practitioner/" rel="noopener noreferrer"&gt;AWS's page for the certification&lt;/a&gt; . I should also mention that the exam has 15 unscored questions, so it is entirely possible that the questions I thought were too difficult for the audience were those unscored questions. The beta exam also has more questions than the standard exam because AWS is evaluating whether the questions are appropriate [ &lt;a href="https://aws.amazon.com/certification/policies/before-testing/#Beta_exams" rel="noopener noreferrer"&gt;Source&lt;/a&gt; ].&lt;/p&gt;

&lt;p&gt;To add more credence to why what I say matters, I'm a part of the AWS Certification SME program and have supported the development of the AWS Certified Developer Associate exam. That adds some additional complexity to sharing my perspective. I know more about how the exams are developed, but I am restricted in what I can share by NDAs. It means I know a little more about what goes on behind the scenes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommended AWS Knowledge
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Familiarity with the core AWS services (for example, Amazon EC2, Amazon S3, AWS Lambda, and Amazon SageMaker) and AWS core services use cases&lt;/li&gt;
&lt;li&gt;Familiarity with the AWS shared responsibility model for security and compliance in the AWS Cloud&lt;/li&gt;
&lt;li&gt;Familiarity with AWS Identity and Access Management (IAM) for securing and controlling access to AWS resources&lt;/li&gt;
&lt;li&gt;Familiarity with the AWS global infrastructure, including the concepts of AWS Regions, Availability Zones, and edge locations&lt;/li&gt;
&lt;li&gt;Familiarity with AWS service pricing models&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The level of AWS knowledge for this exam is similar to that of the Cloud Practitioner exam, based on the exam guide. From my experience with the exam, some questions went beyond "familiarity" with these services and concepts. But, the recommended AWS knowledge section of the exam guide isn't where the focus of questions is drawn from. That's the domains and the task statements under them. I'll go through those next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain 1: Fundamentals of AI and ML
&lt;/h2&gt;

&lt;p&gt;This domain seems like some of the most important information someone with this certification should have, but it only comprises 20% of the scored content. Domains 2 and 3 have a higher percentage of the exam at 24% and 28%, respectively.&lt;/p&gt;

&lt;p&gt;This domain has three task statements:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Task Statement 1.1: Explain basic AI concepts and terminologies.&lt;/li&gt;
&lt;li&gt;Task Statement 1.2: Identify practical use cases for AI.&lt;/li&gt;
&lt;li&gt;Task Statement 1.3: Describe the ML development lifecycle.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have no issues with Task Statement 1.1; it seems ideally suited to this exam. Task Statement 1.2 is also generally well-suited, though I wouldn't expect marketing professionals or line-of-business managers to know much about the capabilities of AWS-managed AI/ML services.&lt;/p&gt;

&lt;p&gt;Task Statement 1.3 and its objectives stretch what I expect the target candidates for this exam to know. I can accept that knowing about the components of an ML pipeline (objective 1.3.1), understanding the sources of ML models (objective 1.3.2), and understanding fundamental concepts of ML operations (MLOps) (objective 1.3.5) would be a benefit for the target candidates that need to know about AI. I expect it's something many of these target roles would need to study specifically for this exam. I don't think that model performance metrics for evaluating ML models (objective 1.3.6) are appropriate. I would expect data engineers and scientists to be doing that and guide what is going to be the best option. Understanding why performance metrics are important is appropriate, but not specific ones, and which ones should be applied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain 2: Fundamentals of Generative AI
&lt;/h2&gt;

&lt;p&gt;Given the hype around GenAI, this domain also seems like a good fit for the exam and has three task statements:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Task Statement 2.1: Explain the basic concepts of generative AI.&lt;/li&gt;
&lt;li&gt;Task Statement 2.2: Understand the capabilities and limitations of generative AI for solving business problems.&lt;/li&gt;
&lt;li&gt;Task Statement 2.3: Describe AWS infrastructure and technologies for building generative AI applications.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Like in Domain 1, Task Statement 2.1 makes sense. The foundation model lifecycle, the last objective for that Task Statement, is something the target candidate roles will probably need to study for the exam. Task Statement 2.2 is perfect for the exam. Anyone with AI or ML credentials should understand the limitations of GenAI. Task Statement 2.3 is also reasonable, for the most part. Some of the target candidate roles will need to specifically study the AWS services and features for developing GenAI applications (objective 2.3.1).&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain 3: Applications of Foundation Models
&lt;/h2&gt;

&lt;p&gt;Again, because of the hype focused on GenAI, questions around foundational models (FMs) and prompt engineering seem reasonable. There are four task statements focused on the FMs domain:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Task Statement 3.1: Describe design considerations for applications that use foundation models.&lt;/li&gt;
&lt;li&gt;Task Statement 3.2: Choose effective prompt engineering techniques.&lt;/li&gt;
&lt;li&gt;Task Statement 3.3: Describe the training and fine-tuning process for foundation models.&lt;/li&gt;
&lt;li&gt;Task Statement 3.4: Describe methods to evaluate foundation model performance.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The task statements seem reasonable at the top level, but once you start looking at their specific objectives, they go beyond what I expect most of the target audience to know or need to know. Why would a marketing professional need to "Identify relevant metrics to assess foundation model performance" such as Recall-Oriented Understudy for Gisting Evaluation (ROUGE), Bilingual Evaluation Understudy (BLEU), or BERTScore? Identifying technical metrics is something engineers should take the lead on. I would expect the stakeholders, such as a marketing professional, to validate that choice by working with the engineers and ensuring business objectives are met. Coincidentally "determine whether a foundation model effectively meets business objectives" is one of the objectives under task statement 3.4.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain 4: Guidelines for Responsible AI
&lt;/h2&gt;

&lt;p&gt;Using AI responsibly is something most people should agree with, and I have been seeing more and more focus on this. It doesn't surprise me to see it on the exam. There are only two task statements for this domain, but task statement 4.1 has a long list of objectives.&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Task Statement 4.1: Explain the development of AI systems that are responsible.&lt;/li&gt;
&lt;li&gt;Task Statement 4.2: Recognize the importance of transparent and explainable models.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;The objectives for this domain are, for the most part, great. They are well suited to ensuring people are thinking about how to use AI responsibly. The only areas that go outside of what I expect the target audience to know are when it comes to tools and AWS features for ensuring responsible AI use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domain 5: Security, Compliance, and Governance for AI Solutions
&lt;/h2&gt;

&lt;p&gt;A security, compliance, and governance domain seems a good idea. Still, it's a topic that can quickly get too detailed for someone who is only supposed to be familiar with AI/ML and does not need an in-depth understanding. There are only two task statements outlined for this domain:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;Task Statement 5.1: Explain methods to secure AI systems.&lt;/li&gt;
&lt;li&gt;Task Statement 5.2: Recognize governance and compliance regulations for AI systems.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;Task Statement 5.1 immediately concerns me. Security is essential with AI and ML, but should someone at a foundational level of understanding be able to explain the methods used to secure an AI system? I don't think that's reasonable. Why should a sales or marketing professional need to know how to secure an AI system? Objective 5.1.3 says, "Describe best practices for secure data engineering (for example, assessing data quality, implementing privacy-enhancing technologies, data access control, data integrity)." This is a foundational AI/ML certification, not the associate-level &lt;a href="https://aws.amazon.com/certification/certified-data-engineer-associate/" rel="noopener noreferrer"&gt;data engineer certification&lt;/a&gt; .&lt;/p&gt;

&lt;p&gt;Task Statement 5.2, at the top level, doesn't concern me, but the objectives seem to exceed the task statement. The task statement says to " &lt;strong&gt;&lt;em&gt;recognize&lt;/em&gt;&lt;/strong&gt; governance and compliance regulations" [emphasis mine], but the objectives ask candidates to identify regulatory standards and describe processes to follow governance protocols. I could see an argument being made that identifying standards counts as recognizing regulations, but describing processes to follow a regulation does not. I can recognize and identify, at least in most cases, when &lt;a href="https://en.wikipedia.org/wiki/Payment_Card_Industry_Data_Security_Standard" rel="noopener noreferrer"&gt;PCI DSS&lt;/a&gt; is relevant, but I can't describe all the processes necessary to be compliant. I expect to have an expert involved to ensure we follow the correct process. The same applies to any significant regulations, including AI-related regulations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What have others said?
&lt;/h2&gt;

&lt;p&gt;I started a chat with other AWS Community Builders, and there seems to be a consensus that the Certified AI Practitioner exam is much more difficult than we expected, especially considering it's a foundational exam. One Community Builder said the questions were foundational in length but required associate levels of knowledge. Many of us wouldn't expect non-technical people to pass the exam, which is a problem considering they are part of the target candidate groups. These conversations within the AWS Community Builder community have led to discussions with AWS. I spoke with two people from the AWS Certifications group and shared direct feedback, including most of what I have written here.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am Saying
&lt;/h2&gt;

&lt;p&gt;The AWS Certified AI Practitioner exam is still in beta and is not in its final form. The questions on the exam will change. Hopefully, the questions will be better suited to the target candidates. I hope the exam guide and its task statements and objectives will change, too. The guide I am looking at is version 1.4 AIF-C01. Creating these exams is hard, and I don't want to discount the incredible effort that went into developing this exam. It's just that there is still work to do. The AWS team is listening to feedback, and I am sure they will adjust this exam.&lt;/p&gt;

&lt;p&gt;I would not recommend this exam to non-technical people right now. Let the final version come out first. If you are going to take the beta exam, study the topics listed in the exam guide. I expect this exam to get easier, so if you have only a foundational level of experience with ML, you should count it as an achievement to pass the beta exam.&lt;/p&gt;

&lt;p&gt;Good luck.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ai</category>
    </item>
    <item>
      <title>IAM Policy Conditions &amp; SQS Queue Access</title>
      <dc:creator>Jason Butz</dc:creator>
      <pubDate>Thu, 22 Feb 2024 14:10:00 +0000</pubDate>
      <link>https://forem.com/aws-builders/iam-policy-conditions-sqs-queue-access-3mn4</link>
      <guid>https://forem.com/aws-builders/iam-policy-conditions-sqs-queue-access-3mn4</guid>
      <description>&lt;p&gt;The other week, I was helping a client work through an interesting challenge. The entire problem resulted from decisions made when the company was designing how they would build and connect their AWS accounts. They had decided there would be two kinds of AWS accounts: one with access to their internal network and one without. The accounts with access to the internal network cannot have any ingress from the public internet; all ingress must be through the corporate network. From a security perspective, I see why they made that choice. They decided to trust AWS and allow ingress to their network from these accounts but disallow ingress from the internet to reduce the chances of an adversary gaining access to their network. For the team I am working with, this presents a problem. They need to integrate with an outside SaaS service and receive webhook requests, but the Kubernetes cluster they are using is part of a platform in one of these accounts without internet ingress. That leaves them unable to receive the webhook events. I proposed we use one of these accounts with public internet ingress to build the integration and then take advantage of the cluster using EKS and supporting IAM Roles to gain access to resources in a different account and integrate with SQS to receive the webhook events.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc5yf2q6s4e3flg0gs7e8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fc5yf2q6s4e3flg0gs7e8.png" alt="AWS architecture diagram showing two AWS accounts. In one account is an API Gateway with an arrow to a Lambda function with an arrow to an SNS Topic with arrows to two SQS Queues. The other account shows an EKS Cluster with two pods, each with an arrow to one of the SQS Queues in the other account." width="753" height="253"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I created an API Gateway with a single endpoint to receive the webhook request from the SaaS application. I built a Lambda function to validate the webhook request, perform limited validation, and then publish the event to an SNS topic. I connected that Lambda to the API Gateway with a Lambda integration. I didn't use a Lambda Authorizer with the API Gateway because validating the webhook required access to the HTTP request's body, which isn't available to Lambda Authorizers. The SNS topic filters messages and ensures only the right messages reach the SQS queues. Overall, it's a simple integration, but it's very effective.&lt;/p&gt;

&lt;h2&gt;
  
  
  Identity &amp;amp; Access Management
&lt;/h2&gt;

&lt;p&gt;I have multiple AWS certifications and good working knowledge of IAM policies, but it sometimes feels like magic. The &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic.html" rel="noopener noreferrer"&gt;Policy Evaluation Logic page in the AWS IAM documentation&lt;/a&gt; is a must-have reference when doing more complex work. One of my weak points with IAM policies is the conditions. I haven't used them enough to be entirely confident, so I go through trial and error. In this case, the SQS resource policy and working with cross-account access meant I spent a lot of time in the IAM documentation.&lt;/p&gt;

&lt;p&gt;The easy part of the queues' resource policy was allowing the SNS topic to send messages. I found an example in AWS's documentation and barely had to think about the conditions. I knew it should "just work." The policy below lets the SNS service send messages to the specified SQS queue when the source is the listed SNS topic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sns.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqs:SendMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sqs:us-east-2:000000000000:MyQueue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ArnEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sns:us-east-2:000000000000:MyTopic"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To allow the pods in the other account access to receive and delete messages, I figured I could do something similar, so I copied and modified the policy statement. I've got an example of what I came up with below. For those of you who know your IAM policies, you might already see where I messed up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sns.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqs:SendMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sqs:us-east-2:000000000000:MyQueue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ArnEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sns:us-east-2:000000000000:MyTopic"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sqs:ReceiveMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqs:DeleteMessage"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sqs:us-east-2:000000000000:MyQueue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ArnLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::999999999999:role/MySemiPredictableRoleName*"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;NOTE:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;This&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;policy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;does&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;work&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;expected;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;use&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;it&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This policy didn't work out; the application running on the pods was logging IAM errors about not having access due to the queue's resource policy. I began working through the policy evaluation logic for IAM, focusing on resource policies. I tried using the &lt;code&gt;ArnEquals&lt;/code&gt; condition type. I tried using the IAM role session instead of the IAM role. I tried changing where the wildcard (&lt;code&gt;*&lt;/code&gt;) was. I couldn't get it to work. So, after about 30 minutes of frustration and redeploying the policy, I started digging into the IAM condition keys.&lt;/p&gt;

&lt;p&gt;Looking into the AWS docs for &lt;code&gt;aws:SourceArn&lt;/code&gt; I was about to find my answer. The &lt;code&gt;aws:SourceArn&lt;/code&gt; condition key is for when making service-to-service requests where the principal is an AWS service. Instead, I needed to use &lt;code&gt;aws:PrincipalArn&lt;/code&gt;. It was all spelled out for me in the documentation. I've included an excerpt from the &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-sourcearn" rel="noopener noreferrer"&gt;AWS documentation&lt;/a&gt; below.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use this key to compare the Amazon Resource Name (ARN) of the resource making a service-to-service request with the ARN that you specify in the policy, but only when the request is made by an AWS service principal. When the source's ARN includes the account ID, it is not necessary to use &lt;code&gt;aws:SourceAccount&lt;/code&gt; with &lt;code&gt;aws:SourceArn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This key does not work with the ARN of the principal making the request. Instead, use &lt;code&gt;aws:PrincipalArn&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As it turned out, &lt;code&gt;aws:SourceArn&lt;/code&gt; is a condition key used to avoid the confused deputy problem during actions between services. The documentation says to only use the condition key in resource-based policies where the &lt;code&gt;Principal&lt;/code&gt; is an AWS service principal. The confused deputy problem is a security issue where an entity without access to a resource coerces a more privileged entity to access the resource. The IAM documentation has a &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html" rel="noopener noreferrer"&gt;great page discussing the confused deputy problem&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now that I knew the correct condition key, the application could access the SQS queues. Below is what my resource policy looked like.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"Service"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sns.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqs:SendMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sqs:us-east-2:000000000000:MyQueue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ArnEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:SourceArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sns:us-east-2:000000000000:MyTopic"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"AWS"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"sqs:ReceiveMessage"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqs:DeleteMessage"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:sqs:us-east-2:000000000000:MyQueue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"ArnLike"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"aws:PrincipalArn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::999999999999:role/MySemiPredictableRoleName*"&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This policy did precisely what I needed, and the application could pull messages from the SQS queues as soon as I deployed my update. It's such a small change, and there isn't anything profound about it. I usually write posts to share something interesting I found, how I do things, or to show something off. This post does show something off, but it's one of my mistakes. It reminds me to double-check that I'm using IAM conditions right, and hopefully, next time, I won't make this same mistake.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>security</category>
    </item>
  </channel>
</rss>
