<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Geoffrey Wiseman</title>
    <description>The latest articles on DEV Community by Geoffrey Wiseman (@geoffreywiseman).</description>
    <link>https://dev.to/geoffreywiseman</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%2F497600%2Fa7c6d16b-e74b-42b5-9cfe-fa37643bd566.jpeg</url>
      <title>DEV Community: Geoffrey Wiseman</title>
      <link>https://dev.to/geoffreywiseman</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/geoffreywiseman"/>
    <language>en</language>
    <item>
      <title>AI-powered Corporate Jargon Translation with Amazon Bedrock</title>
      <dc:creator>Geoffrey Wiseman</dc:creator>
      <pubDate>Wed, 29 Jan 2025 22:53:52 +0000</pubDate>
      <link>https://dev.to/aws-builders/corporate-jargon-translation-with-amazon-bedrock-48ch</link>
      <guid>https://dev.to/aws-builders/corporate-jargon-translation-with-amazon-bedrock-48ch</guid>
      <description>&lt;p&gt;After watching this Under Studio humorous video &lt;a href="https://www.tiktok.com/@understudiohub/video/7446502194737712392?_t=ZM-8tB2ozNcggH&amp;amp;_r=1" rel="noopener noreferrer"&gt;showing a translator for business jargon&lt;/a&gt;, it made me wonder how far off current language models were from being able to do actually do this kind of translation, particularly with the sort of dark humour that makes this work.&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%2F14dmg3uih6xtq4m1mbjh.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%2F14dmg3uih6xtq4m1mbjh.png" alt="Under Studio Thumbnail" width="800" height="835"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So I did some experimentation, crafted a prompt and started testing some models on Amazon Bedrock.&lt;/p&gt;

&lt;h2&gt;
  
  
  Claude 3.5 Haiku
&lt;/h2&gt;

&lt;p&gt;Haiku was the first model I found that did a decent job of translating one of the lines from the video with a one-shot prompt (aka, I gave it one example, and then asked it to do a similar job):&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%2Ffqtkwon7kfqc8ko7znwz.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%2Ffqtkwon7kfqc8ko7znwz.png" alt="Claude 3.5 One-Shot" width="800" height="439"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I tweaked the prompt to request dark humour and got this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We're royally screwed, and management is desperately trying to cover their tracks by throwing around fancy words that essentially mean "we have no clue how to fix this massive mess we've created." The "strategic processes" are basically a corporate version of putting duct tape on a sinking ship while pretending everything is totally fine. Expect a round of scapegoating and middle management sacrifices in 3... 2... 1...&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Since that went well, I tried a real-world example, pulling this paragraph from a Stripe memo from their latest layoff that was leaked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As we've been working through our plans for 2025, leaders took a close look at their organizations and team structures. It became clear that there were several team-level changes needed to make sure we have the right people in the right roles and locations to execute against our plans.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is Haiku's translation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We're about to unleash a bloodbath of layoffs that will make corporate Hunger Games look like a friendly team-building exercise. "Right people in the right roles" is code for "we're cutting jobs, relocating some poor souls, and making everyone who survives do three people's work for the same pay." Expect a wave of panic, LinkedIn update storms, and sudden "networking" coffee meetings. Buckle up, because corporate restructuring is just a fancy term for "we're trimming the fat" - and by "fat," they mean human beings with mortgages and families. Happy 2025, everyone! 🔪📊&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ouch!&lt;/p&gt;

&lt;p&gt;I'm starting to think having a website that can do gallows-humour translation of corporate Jargon would be a fun side project, although I'd probably need to be careful that it didn't rack up massive costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Llama 3.3 70B
&lt;/h2&gt;

&lt;p&gt;Meta's Llama 3.3 70B model also did a fine job. I refined the prompt to request dark humour and got this response to the video line:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Management is freaking out because we're hemorrhaging money, and they're desperately trying to duct-tape together some new "strategies" to stop the bleeding before the whole ship sinks. Think of it as rearranging the deck chairs on the Titanic, but with more PowerPoint presentations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Funny! What about the Stripe paragraph?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Code red: the axe is coming, and HR is sharpening the blade. "Team-level changes" is just corporate speak for "we're about to fire a bunch of people and make the rest of you pick up the slack." So, update your LinkedIn profiles and start practicing your "I'm excited about this new opportunity" face, because it's about to get real.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Awesome. This is definitely a workable side-project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Amazon Nova Premier
&lt;/h2&gt;

&lt;p&gt;The translation from the video:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Management is freaking out because we're missing targets, and they're scrambling to come up with a plan that might just save their jobs. Expect some serious micromanaging and maybe a few sacrificial lambs thrown to the wolves soon.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And from Stripe:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Management's crystal ball showed them that our current team setup is a hot mess. So, brace yourselves for a round of musical chairs where some of you will end up in roles you're not suited for, and others might just find themselves without a chair at all. Welcome to the 2025 restructuring rodeo!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Maybe not quite as funny as Llama, but still pretty decent.&lt;/p&gt;

&lt;p&gt;There's definitely something here. It would be super-funny if you could embed it as a plugin to MS Teams / Zoom as a side-channel for your next all-hands.&lt;/p&gt;

</description>
      <category>genai</category>
      <category>aws</category>
      <category>llm</category>
    </item>
    <item>
      <title>Top re:Invent 2024 Videos</title>
      <dc:creator>Geoffrey Wiseman</dc:creator>
      <pubDate>Thu, 12 Dec 2024 17:04:21 +0000</pubDate>
      <link>https://dev.to/aws-builders/top-reinvent-2024-videos-1b2e</link>
      <guid>https://dev.to/aws-builders/top-reinvent-2024-videos-1b2e</guid>
      <description>&lt;p&gt;Every year, Amazon Web Services holds a conference, "AWS re:Invent", and they release the conference sessions as videos on YouTube. It's a big conference with a lot of sessions, so there are lots of videos to pick from. So much so that it's hard to look at the list of videos and decide which ones you're interested in.  If you have specific topics you want to learn about, you can search for videos on that topic, but looking at the full list is not realistic.&lt;/p&gt;

&lt;p&gt;This year, while looking at the long list of videos, I thought it would be nice to see which are the most popular videos from re:Invent. YouTube doesn't make it very easy to get that list, so I decided to experiment with a custom-built solution. If you want to skip ahead to the working version, it's &lt;a href="https://d3v7hs5izpn29f.cloudfront.net" rel="noopener noreferrer"&gt;available here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  V1: The Experiment
&lt;/h2&gt;

&lt;p&gt;I wrote a short Python script to call the YouTube Data API v3, identify all the videos published to the AWS Events channel in December 2024, and rank by view count.&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%2Fnqrf1pn4j2mnn8p5e37n.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%2Fnqrf1pn4j2mnn8p5e37n.png" alt="V1 Architecture" width="281" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As an experiment, I'd call it a success -- I got a long list of videos, sorted by views. I shared that list in a few places, but I knew that it wasn't complete.  First of all, AWS was still uploading new videos. Secondly, as people continued to watch and share, the view counts would continue to rise, and that would likely impact the rank. In particular, the newer videos had had less time to achieve view count success than the first videos that were posted nearly a week before.&lt;/p&gt;

&lt;p&gt;So, although the experiment was success, I wanted more.&lt;/p&gt;

&lt;h2&gt;
  
  
  V2: Website
&lt;/h2&gt;

&lt;p&gt;If I wanted to run that script to get updated data and be able to share it, the obvious solution was to turn it into single-purpose website that I could share and that anyone could refresh whenever they wanted to get updated data.&lt;/p&gt;

&lt;p&gt;I considered options -- I could turn the Python script into a static site generation script that made HTML and deploy that (e.g. to S3), then schedule a job to generate new content every so often.&lt;/p&gt;

&lt;p&gt;I didn't want to run and schedule the job on my own infrastructure, and since this was an AWS-related project, I decided I'd like it to run on AWS.  I didn't need the job to run super-frequently, so I didn't really want spin up an EC2 instance, so this felt like a good job for Lambda.&lt;/p&gt;

&lt;p&gt;I packaged up the script and the google api client into a Lambda .zip file, uploaded it and began some testing.  I didn't want to have to redeploy to change simple parameters, so I externalized some of the more obvious parameters into environment variables and added those to the Lambda function as well. I didn't really need a load balancer, so I added a Lambda function URL.&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%2F3yq5ffkf1c85uv0d4pvr.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%2F3yq5ffkf1c85uv0d4pvr.png" alt="V2 Architecture" width="281" height="91"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That worked -- but even in testing I was starting to run up against the limitations of the YouTube Data API. In order to prevent abuse (and reduce the cost to their infrastructure), YouTube's Data API has quotas. Each &lt;em&gt;search&lt;/em&gt; call uses 100 units of the 10,000 units you're allocated by default, and getting enough information to rank by views uses at least another unit, so you're limited to less than 100 search calls.  Each search call returns a maximum of 50 results, so rendering the page once was using up nearly a quarter of my quota, and if I were to share this with others, it would almost immediately stop working.&lt;/p&gt;

&lt;h2&gt;
  
  
  V3: Caching
&lt;/h2&gt;

&lt;p&gt;Now we're back to architectural choices. If I'm going to use a cache to avoid hitting the YouTube Data API quota, how am I going to do that?&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Internal Caching&lt;/em&gt;&lt;br&gt;
Do I cache the data from YouTube and only refresh periodically -- once or twice a day? With more intelligent caching, I could even save the video information and on refresh I'd only have to identify new videos and update the video counts on existing videos, which would use far less quota. Then I'd have to store that data somewhere, and since you can't rely on a Lambda staying in memory, that meant an external cache of some kind -- a file on S3, a cache like Redis, or a datastore of some kind, maybe DynamoDB.  I was briefly tempted to test out Aurora DSQL.&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%2Fcz1swajt8iop1gg11we7.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%2Fcz1swajt8iop1gg11we7.png" alt="V3a Architecture" width="281" height="201"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;External Caching&lt;/em&gt;&lt;br&gt;
I could also cache externally -- render the HTML and store that instead of the data used to generate the HTML. That also saves the work to generate the HTML, so it's a bit more efficient, although perhaps a little less flexible in some ways.&lt;/p&gt;

&lt;p&gt;I could go back to a static site generation plan -- schedule the Lambda to run once or twice a day, take the output from the lambda function and store it on S3, where I could serve it up directly.&lt;/p&gt;

&lt;p&gt;The simplest solution seemed to be CloudFront. In theory I could continue to use the Lambda unmodified and CloudFront could cache the results of the Lambda function, and then many page refreshes would simply be CloudFront cache hit.&lt;/p&gt;

&lt;p&gt;This isn't quite as flexible as the internal caching, but it saves me from setting up a scheduled job for static site generation, so I decided to try this path.&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%2Fscs5wxj7082njdvm236n.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%2Fscs5wxj7082njdvm236n.png" alt="V3b Architecture" width="391" height="95"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  V4: Resolving Quota Issues
&lt;/h2&gt;

&lt;p&gt;I published the CloudFront URL to a few places, and heard back from a friend who was travelling that he got an Internal Server error. I suspected the quota had been exceeded, and I checked the logs -- sure enough, it had.  This revealed two problems in V3: error handling, and CloudFront cache plurality.&lt;/p&gt;

&lt;p&gt;I dealt with the error handling first -- I added some code to trap the exception and handle it a little better than the Internal Server Error that was showing. I didn't do anything sophisticated because the goal was to not hit the quota, rather than handle it well.&lt;/p&gt;

&lt;p&gt;The CloudFront problem required a small architectural change -- each CloudFront location has its own cache, and if people from all over the world load the Lambda, each location will invoke the Lambda, and since I can only get away with about four invocations in a day on the quota I had, that was going to be a problem.&lt;/p&gt;

&lt;p&gt;Fortunately, CloudFront has a solution for that -- you can add Origin Shield, which effectively means that CloudFront will also cache internally. If the first request comes in Toronto, the closest CloudFront location will check its own cache, then the internal cache, and then invoke the lambda. CloudFront will then store the result in the internal cache and within the location cache. The second request might come in from Japan, which will get a cache miss in the location-based cache, but a cache hit from the internal cache, and CloudFront will populate the location cache from the internal cache and avoid invoking the lambda a second time.&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%2F6h6475f9alz4vy2u46hz.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%2F6h6475f9alz4vy2u46hz.png" alt="V4 Architecture" width="511" height="105"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This worked well -- the quota errors went away, but then I discovered a new problem. Some of the earliest popular videos were no longer showing in the list.&lt;/p&gt;
&lt;h2&gt;
  
  
  V5: Resolving Dropped Videos
&lt;/h2&gt;

&lt;p&gt;Was it a bug in the code? I did some testing. Turns out, the YouTube Data API pagination only gets you so many pages -- at some point YouTube doesn't give you another next page token, even though there are more videos that match your criteria.&lt;/p&gt;

&lt;p&gt;I tested a few options -- I could break up the month into smaller date ranges and run a search for each range. I was worried this would simply hit the quota faster, but it did seem to work. I had to do a little work to make sure no duplicates showed up, but otherwise it seemed to be ok.&lt;/p&gt;

&lt;p&gt;While I was experimenting with that, I discovered that the YouTube Data API had a feature I missed on the first pass - I could order the results by view count. This meant that I no longer needed to iterate through every video in the month, I could simply use YouTube's own knowledge of the most popular videos that matched the search criteria that I'd established.&lt;/p&gt;

&lt;p&gt;This also meant I could significantly reduce my API traffic -- I could issue just enough search requests to fill my list and stop.  No architectural change, just a code change.&lt;/p&gt;

&lt;p&gt;I tested it, and that worked well.&lt;/p&gt;

&lt;p&gt;Here's the current version of the Python code. There are still improvements that could be made, certainly, but I'm willing to share imperfect code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NamedTuple&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;googleapiclient.discovery&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;googleapiclient.errors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HttpError&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TopVideoConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Configuration for fetching top videos from YouTube.

    Attributes:
        api_key (str): The API key for accessing the YouTube Data API.
        channel_id (str): The ID of the YouTube channel to fetch videos from.
        published_after (str): The start date for fetching videos (ISO 8601 format).
        published_before (str): The end date for fetching videos (ISO 8601 format).
        max_requests (int): The maximum number of API requests to make.
        max_results (int): The maximum number of video results to return.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;published_after&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;published_before&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;max_results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;VideoResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    One record of top videos from YouTube.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;video_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;view_count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TopVideoConfig&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;youtube&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;youtube&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v3&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;developerKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieved_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;videos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title_pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;(^AWS re:Invent 2024\s*-\s*|)&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VideoResult&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;start_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_before&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_search_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Videos: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No more results found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_requests&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Reached max requests limit of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_token&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No more pages found.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retrieved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; videos using &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_requests&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; requests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;view_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;videos&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_search_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VideoResult&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;video_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch_video_ids&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_ids&lt;/span&gt;&lt;span class="p"&gt;)&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieved_ids&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="n"&gt;video_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;video_request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;youtube&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;snippet,statistics&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;video_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform_video&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;video&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_video_ids&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;youtube&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;channelId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;maxResults&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="n"&gt;order&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;viewCount&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;video&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;publishedAfter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_after&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;publishedBefore&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;pageToken&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_token&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;video_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;videoId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;items&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retrieved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; video ids with page token &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;search_requests&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;next_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;nextPageToken&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;video_ids&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;difference&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieved_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transform_video&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;VideoResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;VideoResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;video_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;snippet&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="n"&gt;published_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;snippet&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;publishedAt&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;view_count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;statistics&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;viewCount&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;transform_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;escape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title_pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="c1"&gt;# Load configuration from environment variables
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;TopVideoConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# API Key
&lt;/span&gt;    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;YOUTUBE_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# AWS Events channel ID
&lt;/span&gt;    &lt;span class="n"&gt;channel_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;YOUTUBE_CHANNEL_ID&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Dates
&lt;/span&gt;    &lt;span class="n"&gt;published_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;YOUTUBE_PUBLISHED_AFTER&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;published_before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;YOUTUBE_PUBLISHED_BEFORE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;max_requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MAX_REQUESTS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;1&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;max_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;MAX_RESULTS&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;50&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TopVideoConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;channel_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;published_after&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;published_before&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;render_videos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TopVideoConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;VideoResult&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;title&amp;gt;Top re:Invent 2024 Videos&amp;lt;/title&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;style&amp;gt;body { font-family: sans-serif; } li { padding-bottom: 10px; }&amp;lt;/style&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;h1&amp;gt;Top re:Invent 2024 Videos&amp;lt;/h1&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;h3&amp;gt;(as of &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&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="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;Z&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&amp;lt;/h3&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;ol&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;video&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_results&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;li&amp;gt;&amp;lt;a href=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://youtube.com/watch?v=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;video_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;view_count&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; views)&amp;lt;/li&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/ol&amp;gt;&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;


&lt;span class="c1"&gt;## Bootstrap for Lambda
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_config&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;videos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_videos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;videos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/html; charset=utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;HttpError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;statusCode&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;text/plain; charset=utf-8&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="c1"&gt;## Bootstrap for Direct Testing
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;lambda_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I were making this maintainable, I'd probably split it up a bit more, move the title transformation into the rendering code, add some more error handling improvements, etc.  The HTML could be templated, but I wanted to keep the dependencies low.&lt;/p&gt;

&lt;h2&gt;
  
  
  Done?
&lt;/h2&gt;

&lt;p&gt;It seems to be working and stable.  There are still things I could do to improve it, but it's working well enough that I might move on to the next experiment. It's configurable if I decide to run the same experiment for the next re:Invent or another conference.  I can't run too many of these in parallel without requesting a YouTube Quota increase, but I could certainly run a few.&lt;/p&gt;

&lt;p&gt;I wouldn't mind automating the deployment of it, but there aren't any really interesting parts to that for me, because I've done lots of AWS automation and deployments, so it would mostly be to save myself time, and unless I need to deploy it frequently, it's likely to end up costing me time instead. For an experiment, I'm not sure the automation is warranted.&lt;/p&gt;

&lt;p&gt;I'm also curious to see what the total cost of running this will end up being -- I'm expecting it to be fairly cheap, but we'll see.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>techtalks</category>
      <category>youtube</category>
      <category>python</category>
    </item>
    <item>
      <title>My AWS pre:Invent Highlights: Nov 2024</title>
      <dc:creator>Geoffrey Wiseman</dc:creator>
      <pubDate>Mon, 02 Dec 2024 02:03:13 +0000</pubDate>
      <link>https://dev.to/aws-builders/my-aws-preinvent-highlights-nov-2024-2d4e</link>
      <guid>https://dev.to/aws-builders/my-aws-preinvent-highlights-nov-2024-2d4e</guid>
      <description>&lt;p&gt;The weeks leading up to AWS re:Invent are filled with announcements that Amazon has decided not to hold for either a keynote or a major session.  There are so many that even before re:Invent starts, it’s easy to miss many interesting things.  Of course, I’m sure I’ve missed things, and the things I care about are not necessarily the things that you care about, but even so I’ll share a list of some of the things that I’d recommend you read.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Sessions
&lt;/h2&gt;

&lt;p&gt;Managing AWS root accounts has often been a bit painful. They’re powerful, so you want to keep them locked down, but there are just some places where nothing else works, and you need access to them. This has often meant that security standards recommended Hardware MFA, but as your organization grows, even this gets a bit complicated to manage.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://aws.amazon.com/blogs/aws/centrally-managing-root-access-for-customers-using-aws-organizations/" rel="noopener noreferrer"&gt;Centrally managing root access for customers using AWS Organizations&lt;/a&gt;, it seems like organizations can get rid the liability of having security root accounts while retaining the ability for sufficiently authorized users to open a root session when needs require it.&lt;/p&gt;

&lt;p&gt;This feels a bit like &lt;strong&gt;sudo&lt;/strong&gt;, in a good way.&lt;/p&gt;

&lt;h2&gt;
  
  
  ECS Rebalancing and EC2 Zonal Shift
&lt;/h2&gt;

&lt;p&gt;If you’re an AWS customer, you’ve likely experienced the fear that comes from hearing of any AWS outage — when AWS has an incident, it’s not uncommon for some or many of their customers to be having an incident as well. If that doesn’t happen to you frequently, it will likely take you some time to recover because you’re not experienced at diagnosing and recovering, perhaps you haven’t practice and prepared.&lt;/p&gt;

&lt;p&gt;When that happens, anything AWS can do to simplify that process for you, allow certain things to happen automatically, the better.  ECS &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-ecs-az-rebalancing-speeds-mean-time-recovery-event/" rel="noopener noreferrer"&gt;AZ rebalancing&lt;/a&gt; and &lt;a href="https://aws.amazon.com/blogs/compute/using-zonal-shift-with-amazon-ec2-auto-scaling/" rel="noopener noreferrer"&gt;Zonal Shift for EC2&lt;/a&gt; are both features that can make things a little easier for you when ad things are already happening.&lt;/p&gt;

&lt;h2&gt;
  
  
  EKS Auto Mode
&lt;/h2&gt;

&lt;p&gt;ECS is often my starting point for a containerized service on AWS. Kubernetes is very much the industry standard, but if your team isn’t already up to speed on setting up, managing and using Kubernetes, ECS definitely feels like a lower barrier to entry, a simpler model.  &lt;/p&gt;

&lt;p&gt;Having said that, if you feel like you want to go with Kubernetes, anything AWS can do to simplify the job of making and running Kubernetes infrastructure sounds like a win.  &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/12/amazon-eks-auto-mode/" rel="noopener noreferrer"&gt;EKS auto mode&lt;/a&gt; sounds like it simplifies that experience somewhat, so it’s probably a good starting point for anyone making their first attempt at Kubernetes on AWS.&lt;/p&gt;

&lt;h2&gt;
  
  
  S3 Browsing, Integrity, Conditional Writes
&lt;/h2&gt;

&lt;p&gt;If you’ve built integration with S3 into an application you manage, you’ve likely needed from time to time to access S3 directly, or to offer S3 access to a privilege group of users in your organization. When those users aren’t developers, asking them to use the AWS Console is a high barrier to entry. There are third-party applications, but setting up and maintaining those, credentials and so on is also real work.&lt;/p&gt;

&lt;p&gt;By adding &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/12/aws-transfer-family-web-apps/" rel="noopener noreferrer"&gt;Transfer Family web apps&lt;/a&gt; (&lt;a href="https://aws.amazon.com/blogs/aws/announcing-aws-transfer-family-web-apps-for-fully-managed-amazon-s3-file-transfers/" rel="noopener noreferrer"&gt;including Identity Centre / SSO&lt;/a&gt;) and &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/12/storage-browser-amazon-s3/" rel="noopener noreferrer"&gt;a component&lt;/a&gt; that you can more easily integrate into your own application, AWS is offering ways that might simplify access to S3 when you need it.&lt;/p&gt;

&lt;p&gt;And behind the scenes, &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/12/amazon-s3-default-data-integrity-protections/" rel="noopener noreferrer"&gt;integrity features&lt;/a&gt; using hashes and &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/08/amazon-s3-conditional-writes/" rel="noopener noreferrer"&gt;conditional writes&lt;/a&gt; with both seem very useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aurora Serverless v2: Scaling to Zero
&lt;/h2&gt;

&lt;p&gt;When Aurora Serverless v2 was announced, I know some people were disappointed that it didn’t seem to have the scale-to-zero capability, because one of the things that people value about the “serverless” model is the usage based billing — if you aren’t using it, it doesn’t cost you anything.&lt;/p&gt;

&lt;p&gt;Sounds like that’s &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-aurora-serverless-v2-scaling-zero-capacity/" rel="noopener noreferrer"&gt;back as a feature&lt;/a&gt;, and your Serverless databases can now scale down to 0 ACUs.&lt;/p&gt;

&lt;h2&gt;
  
  
  And More …
&lt;/h2&gt;

&lt;p&gt;Honourable mentions: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/new" rel="noopener noreferrer"&gt;S3 Conditional Writes&lt;/a&gt; are valuable when there might be multiple processes acting in parallel&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/12/amazon-ec2-allowed-amis-enhance-ami-governance/" rel="noopener noreferrer"&gt;Allowed AMIs&lt;/a&gt; and &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-ec2-lineage-information-amis/" rel="noopener noreferrer"&gt;lineage information for AMIs&lt;/a&gt; make for good governance guard-rails.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-q-java-transformation-step-by-step-library-upgrades/" rel="noopener noreferrer"&gt;Amazon Q Java 17 and library upgrades&lt;/a&gt; are good to help with currency / modernization and go hand in hand with the preview of a &lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-q-developer-java-upgrade-transformation-cli-public-preview/" rel="noopener noreferrer"&gt;Java upgrade transformation CLI&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-ebs-time-based-copy-snapshots/" rel="noopener noreferrer"&gt;EBS time-based copy&lt;/a&gt; seems suited for DR planning and audits.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://aws.amazon.com/about-aws/whats-new/2024/11/amazon-connect-email-generally-available/" rel="noopener noreferrer"&gt;Amazon Connect Email&lt;/a&gt; will probably appeal most directly to people already using Amazon Connect telephony, leading to more of an omni-channel solution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the full details, read &lt;a href="https://aws.amazon.com/new/" rel="noopener noreferrer"&gt;What’s New with AWS&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>news</category>
    </item>
    <item>
      <title>AWS AllowList "Update"</title>
      <dc:creator>Geoffrey Wiseman</dc:creator>
      <pubDate>Sat, 16 Nov 2024 16:36:22 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-allowlist-update-3pmc</link>
      <guid>https://dev.to/aws-builders/aws-allowlist-update-3pmc</guid>
      <description>&lt;p&gt;I've just published v1.2.0 of &lt;a href="https://github.com/geoffreywiseman/awswl/" rel="noopener noreferrer"&gt;AWS AllowList&lt;/a&gt; &lt;a href="https://pypi.org/project/awswl/" rel="noopener noreferrer"&gt;to pypi&lt;/a&gt;, an open-source python CLI tool for small AWS accounts to allow users to maintain a security group for SSH access into a VPC.&lt;/p&gt;

&lt;p&gt;Before I get into the changes, let me remind you that while this can be a helpful tool, there are many alternatives that may be better suited for most accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Alternatives
&lt;/h2&gt;

&lt;p&gt;For an AWS environment of any significant size, there are &lt;a href="https://awswl.readthedocs.io/en/latest/alternatives/" rel="noopener noreferrer"&gt;alternatives&lt;/a&gt; that you ought to consider first.  This is a simple solution for simple environments, but it's definitely not what I'd recommend as the best solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's New
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Update Commands
&lt;/h3&gt;

&lt;p&gt;In v1.1.0, I added support to describe and automatically describe additions, but I found that as I went from place to place, it was easy to add new rules with &lt;code&gt;awswl&lt;/code&gt; as I needed them, but that cleaning up past entries was a chore that wasn't directly needed to get work done, so easy to defer -- but also that each old entry was a potential security hole.&lt;/p&gt;

&lt;p&gt;So I decided I ought to have another model in v1.2.0 -- a way to update existing rules (by description) so that the new entry would replace the old one, thus handling the cleanup as part of the process of adding a new entry.&lt;/p&gt;

&lt;p&gt;To this end, I've &lt;a href="https://awswl.readthedocs.io/en/latest/usage/#:~:text=Updating%20an%20Existing%20CIDR" rel="noopener noreferrer"&gt;added two &lt;code&gt;update&lt;/code&gt; commands&lt;/a&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;update&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Replaces an existing security group rule as identified by description with a new rule (same description, newly specified CIDR)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;
&lt;code&gt;update-current&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;Same as &lt;code&gt;update&lt;/code&gt;, but instead of specifying the CIDR block, the CIDR block is automatically generated from your current external IP address.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h3&gt;
  
  
  Subcommands
&lt;/h3&gt;

&lt;p&gt;In v1.1.0, I used CLI options to allow multiple commands to be run on one single invocation of the CLI. That was starting to get increasingly complex to understand and reason about, and I was finding it difficult to add new features without tripping over weird combinations of options.&lt;/p&gt;

&lt;p&gt;So I've replaced the options with subcommands (e.g. git style). So what would have been &lt;code&gt;awswl --add&lt;/code&gt; is now &lt;code&gt;awswl add&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dependencies / Security
&lt;/h3&gt;

&lt;p&gt;There were some security vulnerabilities present on some of the dependencies, so all dependencies have been reviewed and/or updated where possible to ensure that &lt;code&gt;awswl&lt;/code&gt; is modernized to cover any issues that have come up since the last release.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cli</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>AWS Prefix Lists help simplify Networking</title>
      <dc:creator>Geoffrey Wiseman</dc:creator>
      <pubDate>Wed, 03 Jul 2024 14:15:56 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-prefix-lists-help-simplify-networking-1ki4</link>
      <guid>https://dev.to/aws-builders/aws-prefix-lists-help-simplify-networking-1ki4</guid>
      <description>&lt;p&gt;I’ve been working with a client that acquired another company, and has multiple office sites, data centres, and a fair number of private networks. As part of the acquisition, they’ve been working on integrating the systems of the parent and the acquired company, and one element of that has been simplifying the IP address management by removing overlaps between private networks, including AWS VPCs.&lt;/p&gt;

&lt;p&gt;As a result, I’ve been working on setting up some new VPCs, and connecting them into the corporate network. While setting this up I discovered that &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/working-with-managed-prefix-lists.html"&gt;customer-managed prefix lists on AWS&lt;/a&gt; can really simplify some use cases.&lt;/p&gt;

&lt;p&gt;By way of example, imagine a networking situation like this:&lt;/p&gt;

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

&lt;p&gt;With multiple customer networks and multiple VPCs, you can quickly end up needing to add a lot of CIDR blocks to a lot of places, particularly if you only want to target specific ranges, rather than a broad block (like 10.0.0.0/8).  Using specific examples, you might need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add several CIDR ranges to each VPC’s route table to route the traffic to the transit gateway&lt;/li&gt;
&lt;li&gt;add several CIDR ranges to one or more ports in one or more security groups in each VPC in order to allow access from customer networks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the organization adds another site or another CIDR block to an existing site, you might have to go back and find all the places you added those CIDR blocks to, and add it again. This can quickly get tedious.&lt;/p&gt;

&lt;p&gt;This is particularly important if you’re making these changes by hand in the AWS console, either because you don’t have infrastructure setup automated or perhaps because you’re making an exploratory change ahead before changing your infrastructure automation, but even if your infrastructure is fully automated with Terraform, CloudFormation, CDK or Pulumi, you might find that a prefix list makes a security group easier to read.  &lt;/p&gt;

&lt;p&gt;For instance, this security group that allows ICMP, HTTP and HTTPS from a prefix list:&lt;/p&gt;

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

&lt;p&gt;And this one allows it from the CIDR ranges in the diagram above:&lt;/p&gt;

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

&lt;p&gt;I know which of these I find easier to visually inspect. &lt;/p&gt;

&lt;p&gt;So if you’re managing complicated networking on AWS and you haven’t taken a look at prefix lists, I hope I’ve convinced you that it’s time to take a look.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>networking</category>
      <category>vpc</category>
      <category>cloud</category>
    </item>
    <item>
      <title>MySQL v5.7, AWS and Extended Support</title>
      <dc:creator>Geoffrey Wiseman</dc:creator>
      <pubDate>Mon, 01 Jan 2024 02:42:04 +0000</pubDate>
      <link>https://dev.to/aws-builders/mysql-v57-aws-and-extended-support-5325</link>
      <guid>https://dev.to/aws-builders/mysql-v57-aws-and-extended-support-5325</guid>
      <description>&lt;p&gt;If you are using MySQL v5.7.x still for a software project, you likely know that you’re already skating on thin ice.  MySQL v5.7 was released in 2015, and Oracle has just moved the product into its ‘Sustaining Support’ category. However, if you’re using MySQL in AWS RDS,  there are some additional factors to consider.&lt;/p&gt;

&lt;p&gt;Starting 2024-Feb-29, AWS will automatically enroll any MySQL v5.7 databases (RDS or Aurora) into an Extended Support plan, which comes at an additional cost per vCPU-hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic Enrolment
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://aws.amazon.com/about-aws/whats-new/2023/09/amazon-aurora-rds-extended-support-mysql-postgresql-databases/"&gt;original announcement&lt;/a&gt; for &lt;em&gt;extended support&lt;/em&gt; positioned it as an opt-in support feature. Amazon says that they changed their mind due to customer feedback:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;However, we heard lots of feedback from customers that these automatic upgrades may cause their applications to experience breaking changes and other unpredictable behavior between major versions of community DB engines. For example, an unplanned major version upgrade could introduce compatibility issues or downtime if applications are not ready for MySQL 8.0 or PostgreSQL 15.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Anyone who is aware of this impending change would be reasonably likely to understand that if they cannot upgrade, they ought to enrol into &lt;em&gt;Extended Support&lt;/em&gt;, so my guess is that the customers who provided feedback weren’t the ones who were most likely to suffer.  But there’s definitely a tradeoff to be made here. Assuming some percentage of customers are not going to get the message no matter how many emails and announcements you publish, do you want those customers to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Have a new unexpected cost added to the bill?&lt;/li&gt;
&lt;li&gt;Possibly have application downtime after their database gets an upgrade and their software can no longer connect?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Neither experience is great, but I suspect AWS did the math and decided that option #2 is possibly better for customers and potentially better for revenue as well.  Some customers &lt;a href="http://disq.us/p/2x16mf9"&gt;will disagree&lt;/a&gt; of course.&lt;/p&gt;

&lt;h2&gt;
  
  
  Avoiding Extended Support
&lt;/h2&gt;

&lt;p&gt;There’s a way to avoid extended support. Upgrade. Move your existing v5.7.x MySQL Databases to MySQL v8.x. It sounds simple, and for some of you it might be fairly simple.  Anyone currently running MySQL v5.7 on AWS RDS or Aurora ought to at least evaluate that option, and see if that’s feasible. You will, however, need to do a proof-of-concept and some testing to make sure that your application doesn’t have any side-effects of a major version upgrade.&lt;/p&gt;

&lt;p&gt;If you’ve set up your RDS instances manually, this might be something you can test by doing an upgrade in the console. I wouldn’t recommend testing this in production, but I hope most of you have at least one development AWS environment you can use to test this, and if you don’t, this is a good time to consider setting that up.&lt;/p&gt;

&lt;p&gt;If you’re using automation (e.g. GitHub Actions, Terraform, etc), you might need a little bit more time to implement and test the change, but fundamentally it’s the same sort of thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use Extended Support
&lt;/h2&gt;

&lt;p&gt;The main reasons I can see for continuing on MySQL v5.7 past the end of February are these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Upgrade Failed

&lt;ul&gt;
&lt;li&gt;You’ve tried to upgrade to MySQL v8 and you’ve encountered a problem.&lt;/li&gt;
&lt;li&gt;You may be able to fix that before the end of February or you may not.&lt;/li&gt;
&lt;li&gt;If you haven’t resolved all your problems with the upgrade by the end of February you’ll probably know whether or not those problems are worse than paying for extended support.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Opportunity Cost

&lt;ul&gt;
&lt;li&gt;Upgrading MySQL will take some time. Depending on your organization it might take time from:

&lt;ul&gt;
&lt;li&gt;your AWS or DevOps people (doing the upgrade, adjusting automation, etc)&lt;/li&gt;
&lt;li&gt;testers (QA, business users)&lt;/li&gt;
&lt;li&gt;production staff (during deployment, support)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;Those people might already be working on business-critical tasks that you don’t want to take them off. That time comes from somewhere, and you might put a higher value on that than on the Extended Support cost.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Risk

&lt;ul&gt;
&lt;li&gt;This might be organization-specific; the work involved and the perceived risk of doing this before the end of February can vary a lot.&lt;/li&gt;
&lt;li&gt;If you’re a small team, with a small project and you aren’t currently under the gun, two months might seem like way more time than anyone needs to implement and test this change.&lt;/li&gt;
&lt;li&gt;On the other hand, in larger companies with more complexity and that move a little more slowly it might take most of those two months to come up with a good plan for what systems need to be upgraded, which teams will do the work to upgrade, test and roll it all out.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://aws.amazon.com/rds/mysql/pricing/#Amazon_RDS_Extended_Support_costs"&gt;cost varies by region&lt;/a&gt;, by the number of vCPUs (instances * vCPUs/instance) but the current costs are $0.10 - $0.21 per vCPU-hour for the first two years, and roughly double that for the third year. It will depend a lot on how many MySQL instances you’re already using, which &lt;a href="https://aws.amazon.com/rds/instance-types/"&gt;instance types&lt;/a&gt;, and what region you’re running them in.&lt;/p&gt;

&lt;p&gt;I’m working in Canada, so in &lt;code&gt;ca-central-1&lt;/code&gt;, your additional cost for a given instance type would be $0.108/vCPU-hour. That would work out to $72.58 - $80.35 in additional monthly costs per vCPU. If you were running an admittedly massive &lt;code&gt;db.m6i.32xlarge&lt;/code&gt;, that would be upward of $10k for a single instance in extended support costs for a 31-day month.&lt;/p&gt;

&lt;p&gt;Even a tiny instance is non-trivial — a single &lt;code&gt;db.t4g.micro&lt;/code&gt; might cost $0.018 per instance-hour in Canada, but it has two vCPUs, so would cost $0.216 per instance-hour for the extended support. Extended support in this case costs more than 10x the instance cost. About 7.5% of the cost of that instance after March 1st would be for the instance and the remaining 92.5% would be support costs. On larger instances, typically this ratio isn't quite so skewed.  Looking at a few more examples, again using Canadian pricing:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Instance Class&lt;/th&gt;
&lt;th&gt;Instance Cost (HA)&lt;/th&gt;
&lt;th&gt;Extended Support (/hr)&lt;/th&gt;
&lt;th&gt;% of Total Cost for Support&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;db.t4g.micro&lt;/td&gt;
&lt;td&gt;$0.018&lt;/td&gt;
&lt;td&gt;$0.216&lt;/td&gt;
&lt;td&gt;92%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.t4g.small&lt;/td&gt;
&lt;td&gt;$0.035&lt;/td&gt;
&lt;td&gt;$0.216&lt;/td&gt;
&lt;td&gt;86%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.t4g.large&lt;/td&gt;
&lt;td&gt;$0.007&lt;/td&gt;
&lt;td&gt;$0.216&lt;/td&gt;
&lt;td&gt;97%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.t4g.xlarge&lt;/td&gt;
&lt;td&gt;$0.141&lt;/td&gt;
&lt;td&gt;$0.432&lt;/td&gt;
&lt;td&gt;75%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.t4g.2xlarge&lt;/td&gt;
&lt;td&gt;$0.563&lt;/td&gt;
&lt;td&gt;$0.864&lt;/td&gt;
&lt;td&gt;61%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.m6g.large&lt;/td&gt;
&lt;td&gt;$0.168&lt;/td&gt;
&lt;td&gt;$0.216&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.m6g.xlarge&lt;/td&gt;
&lt;td&gt;$0.336&lt;/td&gt;
&lt;td&gt;$0.432&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.m6g.2xlarge&lt;/td&gt;
&lt;td&gt;$0.672&lt;/td&gt;
&lt;td&gt;$0.864&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.m6g.4xlarge&lt;/td&gt;
&lt;td&gt;$1.344&lt;/td&gt;
&lt;td&gt;$1.728&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.m6g.8xlarge&lt;/td&gt;
&lt;td&gt;$2.688&lt;/td&gt;
&lt;td&gt;$3.456&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.m6g.12xlarge&lt;/td&gt;
&lt;td&gt;$4.032&lt;/td&gt;
&lt;td&gt;$5.184&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.m6g.16xlarge&lt;/td&gt;
&lt;td&gt;$5.376&lt;/td&gt;
&lt;td&gt;$6.912&lt;/td&gt;
&lt;td&gt;56%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.r6g.large&lt;/td&gt;
&lt;td&gt;$0.278&lt;/td&gt;
&lt;td&gt;$0.216&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.r6g.xlarge&lt;/td&gt;
&lt;td&gt;$0.556&lt;/td&gt;
&lt;td&gt;$0.432&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.r6g.2xlarge&lt;/td&gt;
&lt;td&gt;$1.113&lt;/td&gt;
&lt;td&gt;$0.864&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.r6g.4xlarge&lt;/td&gt;
&lt;td&gt;$2.226&lt;/td&gt;
&lt;td&gt;$1.728&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.r6g.8xlarge&lt;/td&gt;
&lt;td&gt;$4.451&lt;/td&gt;
&lt;td&gt;$3.456&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.r6g.12xlarge&lt;/td&gt;
&lt;td&gt;$6.677&lt;/td&gt;
&lt;td&gt;$5.184&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;db.r6g.16xlarge&lt;/td&gt;
&lt;td&gt;$8.903&lt;/td&gt;
&lt;td&gt;$6.912&lt;/td&gt;
&lt;td&gt;44%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's worth noting that this isn't considering the cost of storage, backups, snapshots, and also isn't factoring in reserved instances or savings plans. I've also left out the high-availability options for now. There are so many variables in AWS cost projections that it's very difficult to be comprehensive.&lt;/p&gt;

&lt;p&gt;I’ll do a case study below with specific costs for a particular scenario, to get a sense of what it might look like with multiple database instances of different classes.&lt;/p&gt;

&lt;p&gt;It’s going to add up, so if you can upgrade, you probably need to start looking at your options to do that.&lt;/p&gt;

&lt;h2&gt;
  
  
  End of Extended Support
&lt;/h2&gt;

&lt;p&gt;Extended Support doesn’t last forever. It will help bridge the gap if moving off of MySQL v5.7 is hard for you, but at the moment RDS Extended Support for MySQL v5.7 is &lt;a href="https://aws.amazon.com/blogs/aws/your-mysql-5-7-and-postgresql-11-databases-will-be-automatically-enrolled-into-amazon-rds-extended-support/"&gt;expected to be available until 2027-Feb-28&lt;/a&gt;. If upgrading from (or switching away from) MySQL v5.7 is a challenge, you’re still going to want to put it in your roadmap for sometime in the next few years, otherwise you’ll be facing an even harder challenge in 2027.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should I do?
&lt;/h2&gt;

&lt;p&gt;First, do the math. How many MySQL v5.7 instances do you have? How many vCPUs do those have? What’s the additional cost per month that you’ll have to pay under Extended Support?&lt;/p&gt;

&lt;p&gt;Next, set some time aside. Attempt an upgrade in a local environment or in a development environment.  Is it easy? Does it seem to work? A quick proof-of-concept isn’t necessarily enough to move forward, but it should give you some sense of how much work you have ahead.&lt;/p&gt;

&lt;p&gt;Then evaluate the tradeoff. Can you afford the time required to make the changes, test them and deploy them? How much time will that take in your organization? What else is your team working on and can you afford that time? What’s the opportunity cost of doing the upgrade when compared to the support cost of not doing the upgrade?&lt;/p&gt;

&lt;p&gt;And if you can’t do the upgrade by the end of February, when can you do it? How much will that support cost add up to in three months, or six months?&lt;/p&gt;

&lt;h2&gt;
  
  
  Case Study
&lt;/h2&gt;

&lt;p&gt;I have a client in this situation. They had planned to move off MySQL v5.7 in October, but those plans were derailed, and further testing on a proof-of-concept has revealed problems that don’t have an identified fix — it’s a weird problem, and it only seems to be exposed in the cloud environments, but it’s going to take some work to diagnose and fix.&lt;/p&gt;

&lt;p&gt;In addition, this client has a lot of work already in flight on the project, and the opportunity cost of delaying that work may be larger than the cost per month they’ll currently have to pay in extended support.&lt;/p&gt;

&lt;p&gt;So if they were running:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;5 &lt;code&gt;t3.micro&lt;/code&gt; instances

&lt;ul&gt;
&lt;li&gt;$0.037 per hour each&lt;/li&gt;
&lt;li&gt;2 vCPUs, so $0.216 per hour each&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;8 &lt;code&gt;t2.micro&lt;/code&gt; instances

&lt;ul&gt;
&lt;li&gt;$0.037 per hour each&lt;/li&gt;
&lt;li&gt;1 vCPU, so $0.108 per hour each&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;1 &lt;code&gt;m5.2xlarge&lt;/code&gt; instance

&lt;ul&gt;
&lt;li&gt;$1.512 per hour&lt;/li&gt;
&lt;li&gt;8 vCPU, so $0.864 per hour&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If I’m doing the math correctly, I could project their bill for the next six months as:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Month&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Jan&lt;/td&gt;
&lt;td&gt;$1,483.79&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Feb&lt;/td&gt;
&lt;td&gt;$1,406.69&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mar&lt;/td&gt;
&lt;td&gt;$2,366.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Apr&lt;/td&gt;
&lt;td&gt;$2,290.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;May&lt;/td&gt;
&lt;td&gt;$2,366.66&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jun&lt;/td&gt;
&lt;td&gt;$2,290.32&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Effectively, starting March, the database cost will go up roughly 65% until they can find the time to resolve the issues and upgrade. &lt;/p&gt;

&lt;h2&gt;
  
  
  What about you?
&lt;/h2&gt;

&lt;p&gt;Were you able to migrate in time? Are you already working towards it? Or have you decided that you're going to be using Extended Support already?&lt;/p&gt;

</description>
      <category>aws</category>
      <category>awsrds</category>
      <category>economics</category>
      <category>cloud</category>
    </item>
    <item>
      <title>AWS ECS Features for Restart Loops</title>
      <dc:creator>Geoffrey Wiseman</dc:creator>
      <pubDate>Wed, 05 Oct 2022 21:50:46 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-ecs-features-for-restart-loops-m67</link>
      <guid>https://dev.to/aws-builders/aws-ecs-features-for-restart-loops-m67</guid>
      <description>&lt;p&gt;I had a client get an &lt;a href="https://dev.to/aws-builders/nat-gateway-charges-from-ecs-service-restart-3p1c"&gt;unexpectedly high AWS bill&lt;/a&gt; whose fundamental cause was an AWS ECS service stuck in a restart loop. Although I’ve encountered these before, I decided to dig a little deeper and write up a series of blog posts on the subject.&lt;/p&gt;

&lt;p&gt;On the &lt;a href="https://dev.to/aws-builders/aws-ecs-restart-loops-1lj4"&gt;previous post&lt;/a&gt;, I described in more detail what an ECS Restart Loop is, and what it looks like, for people who haven’t encountered one, or haven’t spent much time looking into them.&lt;/p&gt;

&lt;p&gt;Now I’m going to talk about AWS ECS features that can help deal with restart loops: service throttling and circuit breakers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment Circuit Breakers
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.amazonaws.cn/en_us/AmazonECS/latest/userguide/deployment-type-ecs.html" rel="noopener noreferrer"&gt;Deployment circuit breakers&lt;/a&gt; were introduced to ECS in &lt;a href="https://aws.amazon.com/blogs/containers/announcing-amazon-ecs-deployment-circuit-breaker/" rel="noopener noreferrer"&gt;late 2020&lt;/a&gt;. These work reasonably well if your infinite restarts are caused by a broken deployment, and can even roll back your service to the previous working state.&lt;/p&gt;

&lt;p&gt;This is what the ECS events look like with a failed deployment:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ahqrlt4k22vvrgq5ct0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0ahqrlt4k22vvrgq5ct0.png" alt="ECS Circuit Breaker"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's worth pointing out that the failure threshold (10 failures minimum) can take a while to reach, particularly on small services. This depends in part on how long it takes for a single health check to fail, but in extreme cases particularly for services with a very small number of tasks, it might take &lt;a href="https://github.com/aws/containers-roadmap/issues/1573" rel="noopener noreferrer"&gt;up to an hour&lt;/a&gt; for the circuit breaker to kick in.&lt;/p&gt;

&lt;p&gt;Deployment circuit breakers solve the most common case and are easy to turn on, so this is a great starting point. If you're using ECS and you haven't looked into circuit breakers, I recommend you do so now.&lt;/p&gt;

&lt;p&gt;There are limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not all infinite restart problems happen during deployment&lt;/li&gt;
&lt;li&gt;It's still possible to create an ECS service with deployment circuit breakers disabled&lt;/li&gt;
&lt;li&gt;The failure threshold is not configurable&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Throttling
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/service-throttle-logic.html" rel="noopener noreferrer"&gt;ECS Service Throttle Logic&lt;/a&gt; can help in the specific scenario that an ECS service does not reach the &lt;code&gt;RUNNING&lt;/code&gt; state, so that when ECS tries to start a service and it cannot be started, it will throttle the rate at which it re-attempts to start the container.&lt;/p&gt;

&lt;p&gt;The limitations here are important:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only employed if a task goes from &lt;code&gt;PENDING&lt;/code&gt; to &lt;code&gt;STOPPED&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;More common if a task does not have the necessary resources, or the docker image can't be pulled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This can be helpful outside of a deployment context, but in most scenarios where I've encountered ECS service restart loops, it would not have helped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recommendations
&lt;/h2&gt;

&lt;p&gt;Firstly, if you're not already using Circuit Breakers, you probably should be. Unless you've already evaluated circuit breakers and decided they aren't right for your environment, I'd recommend enabling these by default. If you have organizational defaults in a tool like Terraform modules, CDK or Pulumi, make enabling circuit breakers your default.&lt;/p&gt;

&lt;p&gt;Secondly, the place where I've seen this the most is during deployment. If you're automating deployments, you might want to consider making sure that ECS considers the deployment successful and complete before your automation finishes. This might mean the automation needs to wait for the service deployment to be complete and the service to be healthy, which can take time, but it’s better than doing a deployment and assuming everything’s ok until you discover later than it’s not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is this enough?
&lt;/h2&gt;

&lt;p&gt;So if ECS has features to deal with restart loops, is that enough? Is the problem solved? No.&lt;/p&gt;

&lt;p&gt;Circuit breakers and deployment automation will help with failed deployments, but that's not the only reason an ECS service might become unhealthy.&lt;/p&gt;

&lt;p&gt;They're also the something  that you have to put in place, a precaution you have to deliberately take. In lots of organizations, you might have many different workloads built by different teams. If a team spins up their first ECS service and doesn’t turn on circuit breakers, will you catch that? Do you have the right policies in place to make sure that can’t happen?  Or do you have monitoring to catch it when it does?&lt;/p&gt;

&lt;p&gt;That’s the topic of the next post: Monitoring AWS for ECS Restart Loops.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ecs</category>
      <category>cloud</category>
    </item>
    <item>
      <title>AWS ECS Restart Loops</title>
      <dc:creator>Geoffrey Wiseman</dc:creator>
      <pubDate>Thu, 11 Aug 2022 15:37:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/aws-ecs-restart-loops-1lj4</link>
      <guid>https://dev.to/aws-builders/aws-ecs-restart-loops-1lj4</guid>
      <description>&lt;p&gt;When you use Amazon Web Services (AWS) Elastic Container Service (ECS), you may encounter a situation where ECS tries to deploy one of your ECS services, but your tasks are unhealthy, so it tries again.&lt;/p&gt;

&lt;p&gt;And again. And again. And again. And again. &lt;/p&gt;

&lt;p&gt;ECS is very determined to start healthy tasks for you, and if you're not careful it may continue to try relentlessly, for days, weeks, months.&lt;/p&gt;

&lt;p&gt;In the process, it may be creating log streams, allocating elastic network interfaces, downloading container images from the internet, incurring NAT gateway charges, making AWS config recordings, logging CloudTrail events and EventBridge events.&lt;/p&gt;

&lt;p&gt;In an extreme case, this could &lt;a href="https://dev.to/aws-builders/nat-gateway-charges-from-ecs-service-restart-3p1c"&gt;trigger a pretty extreme spike in billing&lt;/a&gt; that might take you hours to track down.&lt;/p&gt;

&lt;p&gt;I'm going to write up a few ways to avoid ECS restart loops and to detect them so that you can intervene in the next couple of posts, but in case this is unfamiliar territory, I'll start with some definitions in and a demonstration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Definitions
&lt;/h2&gt;

&lt;p&gt;Amazon Web Services (AWS) has a number of approaches to deploy and run software applications in a containerized model. &lt;a href="https://aws.amazon.com/ecs/" rel="noopener noreferrer"&gt;Elastic Container Service (ECS)&lt;/a&gt; is one of the oldest and simplest AWS services to run software in a container, and it's usually what I'd recommend for someone getting into containers on AWS for the first time.&lt;/p&gt;

&lt;p&gt;When you want ECS to run containers, you define a ECS Task: essentially a deployable unit with metadata about the container(s) to run, how they should be networked, what resources they need. &lt;/p&gt;

&lt;p&gt;At that point you can ask ECS to create a task using that definition on an ECS/EC2 instance you control or on AWS Fargate, but typically you're more likely to wrap that ECS task in an ECS Service, which will make it easy to run a number of tasks in parallel, often in different availability zones, load balance them, restart them if they fail health checks, and so on.&lt;/p&gt;

&lt;p&gt;The AWS ECS documentation has a &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/developerguide/welcome-features.html" rel="noopener noreferrer"&gt;Amazon ECS Components&lt;/a&gt; section that describes the elements of ECS if you're new to all of this.&lt;/p&gt;

&lt;p&gt;One of the capabilities that ECS services add is resilience. What happens if the EC2 instance your task running on becomes unresponsive or is terminated? ECS can detect that and replace that task with another one running on another instance. What if your task has a memory leak and crashes? ECS can detect that your task is unhealthy and shut it down and start another. Many of the ways that ECS can intervene to keep your service healthy and robust revolve around shutting down and starting up the containerized tasks you've defined.&lt;/p&gt;

&lt;p&gt;But ECS doesn't know the internals of your application. It doesn't know why your task is shutting down or failing health checks. So if it starts up a task and that task also fails, all it can do is keep trying. If it tries repeatedly and the tasks fail repeatedly, that's a restart loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restart Loop
&lt;/h2&gt;

&lt;p&gt;Why might a task fail and when?  There are lots of scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The cluster might not have the necessary resources to start the task, causing the task to fail during startup, never reaching the &lt;code&gt;RUNNING&lt;/code&gt; state.&lt;/li&gt;
&lt;li&gt;The container might contain a crashing bug, causing the process to exit on startup.&lt;/li&gt;
&lt;li&gt;The container might contain a crashing bug that requires time or a particular scenario to occur, causing the process to exit after being healthy and functional for some time.&lt;/li&gt;
&lt;li&gt;The container might be healthy, but contain a faulty health check, causing ECS to believe the task is not healthy. This would allow the task to start, but ECS will terminate the task after running the initial health checks.&lt;/li&gt;
&lt;li&gt;The container might be healthy and health check might be correctly defined, but the security group might prevent the health check from reaching the container, causing ECS to terminate the task after health checks.&lt;/li&gt;
&lt;li&gt;The container's health check might depend on an external resource that isn't available. This could happen at any time, causing ECS to terminate the task. (Incidentally, this is a good reason to isolate health checks from the environment.)&lt;/li&gt;
&lt;li&gt;Something about the environment (VPC, networking, security groups, etc.) might change in a way that causes the container or the health check to fail. This could happen anytime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And what should ECS do when a task fails? If that task is part of a service, ECS is supposed to maintain a certain number of healthy tasks (as configured in the &lt;code&gt;desiredTasks&lt;/code&gt; parameter). If a task terminates, it should be replaced by starting a new task. If a task fails health checks, it should be terminated and replaced.&lt;/p&gt;

&lt;p&gt;But what if a task is replaced and the replacement also fails immediately? Should ECS continue to try? For how long? There's no one right answer here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If there's a problem with the container or health check, the task will never succeed, and ECS is wasting cloud resources trying to "fix" a service that is fundamentally broken.&lt;/li&gt;
&lt;li&gt;If there's a temporary problem with the environment or the dependency, restarting the task may not be helpful, but the service may return to health when the temporary problem ends.&lt;/li&gt;
&lt;li&gt;If the task is flaky (occasionally crashes, memory leak, etc), restarting the task each time it fails is probably the best choice.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But should ECS detect that replacing tasks isn't working and change its behaviour? Perhaps. Until recently, ECS would just keep trying by terminating and replacing tasks indefinitely. &lt;/p&gt;

&lt;p&gt;There was always a way to manually intervene -- even if you don't know what's causing the restart loop, you might decide that the restarts aren't helping. If you want it to stop, you can set &lt;code&gt;desiredTasks&lt;/code&gt; to &lt;code&gt;0&lt;/code&gt;. This means that when a task fails, ECS wouldn't need to replace that task, ending the restart cycle.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demonstration
&lt;/h2&gt;

&lt;p&gt;What does it look like when an ECS service goes into a restart loop? If you look at an ECS service in a restart loop, you'll see a series of events to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;start / stop tasks&lt;/li&gt;
&lt;li&gt;register / degregister targets from load balancer &lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;If the failure happened during a deployment, you might also see a deployment in progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next?
&lt;/h2&gt;

&lt;p&gt;So if you're having a problem with ECS services going into a restart loop or you're simply wondering how to protect yourself from it, what should you do?&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/aws-builders/aws-ecs-features-for-restart-loops-m67"&gt;second post&lt;/a&gt;, I'm going to go over some the new features added to ECS in the last few years that can help deal with service restarts, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;service throttling&lt;/li&gt;
&lt;li&gt;circuit breakers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And in the third post, I'm going to discuss some of the options for monitoring ECS to detect service restarts.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>ecs</category>
      <category>containers</category>
    </item>
    <item>
      <title>Tracking down a spike in NAT Gateway charges</title>
      <dc:creator>Geoffrey Wiseman</dc:creator>
      <pubDate>Wed, 27 Jul 2022 15:11:00 +0000</pubDate>
      <link>https://dev.to/aws-builders/nat-gateway-charges-from-ecs-service-restart-3p1c</link>
      <guid>https://dev.to/aws-builders/nat-gateway-charges-from-ecs-service-restart-3p1c</guid>
      <description>&lt;p&gt;If you spend enough time architecting and building solutions for the cloud, you're likely to have either experienced or at the very least, heard tell of an exciting cloud billing story (📈).&lt;/p&gt;

&lt;p&gt;This is my tale of investigating an unexpected spike in NAT Gateway charges in a cloud migration project.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project
&lt;/h2&gt;

&lt;p&gt;One of my clients has recently migrated a complex on-premise application to AWS, a lift-and-shift project that uses GitHub Actions, Terraform and Ansible to automate the creation of environment-specific VPCs containing several web applications, several microservices, and supporting infrastructure.&lt;/p&gt;

&lt;p&gt;After the new environments were launched, the project team took some time to step back and look for problems. We found and documented points of friction in the automation, we tightened the security, and we looked at the month by month bills from AWS.&lt;/p&gt;

&lt;p&gt;Much of the AWS costs were as expected, but there was one category that we didn't have an explanation for: NAT.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's NAT?
&lt;/h2&gt;

&lt;p&gt;NAT stands for "Network Address Translation". You typically need NAT when you're trying to communicate between a resource that does not have a public IP address and some outside resource. Because the internal machine does not have a public IP address, there's no way to communicate directly, so you can have another resource (a server, or in this case, an AWS-managed &lt;a href="https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html" rel="noopener noreferrer"&gt;NAT Gateway&lt;/a&gt;) translate between an external address and port to an internal address and port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Unexplained NAT Costs
&lt;/h2&gt;

&lt;p&gt;The VPCs created by the project's automation have private subnets and those subnets use NAT Gateways to communicate with the outside world when necessary.&lt;/p&gt;

&lt;p&gt;However, the NAT costs were high and rising:&lt;/p&gt;

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

&lt;p&gt;The NAT traffic was surprisingly high -- we were approaching nearly 40TB of monthly NAT, way more than the team would have expected. Where was the traffic coming from, and where was it going to, and why?&lt;/p&gt;

&lt;p&gt;There's a VPC per environment, and some of the environments share an account, so let's see if each VPC is seeing similar amounts of NAT traffic by using CloudWatch Metrics for the managed NAT gateways.&lt;/p&gt;

&lt;h2&gt;
  
  
  CloudWatch Metrics
&lt;/h2&gt;

&lt;p&gt;Let's look at the metrics for NAT Gateways in CloudWatch. "NATGateway &amp;gt; NAT Gateway Metrics" has a bunch of metrics, but since we're looking at data volume, let's turn on all the ones that refer to "Bytes":&lt;/p&gt;

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

&lt;p&gt;Ok, it's just one NAT Gateway that seems to be responsible for the majority of the traffic, both &lt;code&gt;BytesInFromDestination&lt;/code&gt; and &lt;code&gt;BytesOutToSource&lt;/code&gt;. Both of those suggest that the traffic is coming from an outside source -- that resources inside the VPC are downloading large amounts of data:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  VPC Flow Logs / CloudWatch Insights
&lt;/h2&gt;

&lt;p&gt;Now that we've narrowed it down to one VPC, how do we figure out where the traffic is coming from and going to? A good next step would be VPC Flow Logs, which log traffic in your VPC. VPC flow logs were already turned on, and there's a convenient &lt;a href="https://aws.amazon.com/premiumsupport/knowledge-center/vpc-find-traffic-sources-nat-gateway/" rel="noopener noreferrer"&gt;knowledge-base article&lt;/a&gt; that gives tips on how to use CloudWatch Log Insights to analyze contributors to traffic through a NAT Gateway.&lt;/p&gt;

&lt;p&gt;Looking at traffic going in and out of the NAT gateway, it looks like there's a lot of different internal IP Addresses involved:&lt;/p&gt;

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

&lt;p&gt;So what's the source of this data?&lt;/p&gt;

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

&lt;p&gt;A quick scan suggests that these are AWS addresses, likely S3. So a variety of internal IP addresses in a single VPC are downloading lots of data from AWS S3 instances. Why?&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracking Down IP Addresses
&lt;/h2&gt;

&lt;p&gt;At this point, I checked a couple of things first -- in particular, I was starting to be suspicious that there might be an unhealthy ECS service that wasn't being well-monitored, but I didn't see one on a very quick scan, so I decided to continue with the systematic search. What are these IP Addresses associated with?&lt;/p&gt;

&lt;p&gt;So, now we look in EC2 &amp;gt; Network Interfaces (ENIs):&lt;/p&gt;

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

&lt;p&gt;Nothing. I searched for several of the ip addresses listed in the flow logs, and none of them are currently active. Suspicious. Something is using an IP address, doing some NAT traffic and then ... going away before I'm searching for it.&lt;/p&gt;

&lt;p&gt;Can we find the IP address on CloudTrail?&lt;/p&gt;

&lt;p&gt;Yes we can:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

❯ aws cloudtrail lookup-events --max-results 20 --lookup-attributes AttributeKey=EventName,AttributeValue=CreateNetworkInterface --end-time "2022-06-10" | jq '.Events[].CloudTrailEvent | fromjson' | jq .userAgent| sort | uniq -c
  20 "ecs.amazonaws.com"


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Conveniently, the security group which appears in the event also matches the ECS service name. Sounds like my theory was correct, I just hadn't found the right service yet.&lt;/p&gt;

&lt;p&gt;It's immediately clear from the service events that the service isn't healthy and has been restarting since some event in March without anyone noticing: &lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;

{
    "id": "715c6493-9a08-4de2-82d1-afcaf54c54ef",
    "createdAt": "2022-07-13T15:24:18.650000-04:00",
    "message": "(service qa-XXX-service) deregistered 1 targets in (target-group arn:aws:elasticloadbalancing:ca-central-1:004110273183:targetgroup/qa-XXX-service/a7f603824fb1e497)"
},
{
    "id": "0fea699a-3628-4f9d-abe0-dd89a930aa45",
    "createdAt": "2022-07-13T15:24:01.939000-04:00",
    "message": "(service qa-XXX-service) has begun draining connections on 1 tasks."
},
{
    "id": "68e666d1-f489-40a3-9dda-c5b739a7e6ad",
    "createdAt": "2022-07-13T15:24:01.926000-04:00",
    "message": "(service qa-XXX-service) deregistered 1 targets in (target-group arn:aws:elasticloadbalancing:ca-central-1:004110273183:targetgroup/qa-XXX-service/a7f603824fb1e497)"
},
{
    "id": "3cca6545-d38d-4b88-a8b9-1e936f737526",
    "createdAt": "2022-07-13T15:23:30.948000-04:00",
    "message": "(service qa-XXX-service) registered 1 targets in (target-group arn:aws:elasticloadbalancing:ca-central-1:004110273183:targetgroup/qa-XXX-service/a7f603824fb1e497)"
},
{
    "id": "36b50859-babe-47eb-8dcb-ca2c5c661491",
    "createdAt": "2022-07-13T15:23:21.103000-04:00",
    "message": "(service qa-XXX-service) has started 1 tasks: (task d1a70a3629d64febbc91695b4b848d71)."
},
{
    "id": "32f354a3-21ec-4a26-aac0-ceb02805dd16",
    "createdAt": "2022-07-13T15:23:16.401000-04:00",
    "message": "(service qa-XXX-service) has begun draining connections on 1 tasks."
},


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Each time it restarts, it's pulling down a new container image from ECS, and incurring NAT bandwidth costs to do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Now?
&lt;/h2&gt;

&lt;p&gt;First, stop the bleeding:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set the number of desired tasks to &lt;code&gt;0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;This will stop ECS from trying to create new tasks that will never be healthy.&lt;/li&gt;
&lt;li&gt;This is a good way to stop ECS from trying to fix a broken service without having to significantly alter the task definition or service definition, allowing you to diagnose later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, we can monitor NAT Gateway metrics to make sure the usage has dropped, and AWS billing costs over the next few days to make sure the NAT Gateway costs have dropped.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next?
&lt;/h2&gt;

&lt;p&gt;We've solved the immediate problem, but it has identified lots of areas for further investigation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Technical Debt

&lt;ul&gt;
&lt;li&gt;This is an old service that has been replaced by the new environments but hasn't been cleaned up.&lt;/li&gt;
&lt;li&gt;Those cleanup tasks that had been deferred now have a very real cost.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Better Monitoring of ECS Services

&lt;ul&gt;
&lt;li&gt;I've seen an ECS service go into this infinite unhealthy loop before.&lt;/li&gt;
&lt;li&gt;There are lots of ways to detect and diagnose failing ECS services, but most of them will require some setup on your part.&lt;/li&gt;
&lt;li&gt;These NAT costs make it clear that the project team will need to invest more time into detecting unhealthy ECS services.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;NAT Gateway Monitoring

&lt;ul&gt;
&lt;li&gt;One NAT gateway was doing significantly more traffic than the others&lt;/li&gt;
&lt;li&gt;Could add an alarm for when NAT gateway usage passes a reasonable limit.&lt;/li&gt;
&lt;li&gt;Could also consider &lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Anomaly_Detection.html" rel="noopener noreferrer"&gt;CloudWatch anomaly detection&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Cost Monitoring

&lt;ul&gt;
&lt;li&gt;A cost of this magnitude should probably not have gone unnoticed for months.&lt;/li&gt;
&lt;li&gt;While the other items still have value, this is just one way out of a seemingly infinite number of ways your AWS bill could be surprising. If we improve our monitoring of ECS services and NAT gateways, that won't prevent something else from going off the rails in a month or two.&lt;/li&gt;
&lt;li&gt;There are lots of options here:

&lt;ul&gt;
&lt;li&gt;Manually reviewing AWS bills when they come in&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/monitor_estimated_charges_with_cloudwatch.html" rel="noopener noreferrer"&gt;Billing threshold alarms&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aws.amazon.com/aws-cost-management/aws-cost-anomaly-detection/" rel="noopener noreferrer"&gt;Cost anomaly detection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Third party solutions to manage and optimize AWS costs.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;PrivateLink

&lt;ul&gt;
&lt;li&gt;Without PrivateLink, traffic between ECS tasks running on Fargate and AWS services like ECS and S3 are travelling through the NAT gateway.&lt;/li&gt;
&lt;li&gt;By &lt;a href="https://aws.amazon.com/blogs/compute/setting-up-aws-privatelink-for-amazon-ecs-and-amazon-ecr/" rel="noopener noreferrer"&gt;configuring PrivateLink&lt;/a&gt; for the VPC, we can route that traffic through AWS internal infrastructure, reducing NAT gateway traffic and costs.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Replace Managed NAT Gateways

&lt;ul&gt;
&lt;li&gt;It is possible to set up your own NAT gateways on EC2 instances instead of using the AWS-managed ones.&lt;/li&gt;
&lt;li&gt;This would be more work to setup and maintain, and I'm not sure it's worth it, but there's an argument to be made.&lt;/li&gt;
&lt;li&gt;I will admit to having some sympathy for Corey Quinn's "&lt;a href="https://www.lastweekinaws.com/blog/the-aws-managed-nat-gateway-is-unpleasant-and-not-recommended/" rel="noopener noreferrer"&gt;Unpleasant and Not Recommended&lt;/a&gt;" tagline.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;None of these are immediately critical, but if we don't address them, these and similar problems will recur and bring with them new and exciting AWS bill problems.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>awsbill</category>
      <category>debug</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Intro to EC2 Global View</title>
      <dc:creator>Geoffrey Wiseman</dc:creator>
      <pubDate>Wed, 06 Oct 2021 14:51:27 +0000</pubDate>
      <link>https://dev.to/aws-builders/intro-to-ec2-global-view-5cnb</link>
      <guid>https://dev.to/aws-builders/intro-to-ec2-global-view-5cnb</guid>
      <description>&lt;p&gt;If you've spent more than a few days using the AWS console, you've probably had a moment where you've found yourself staring at an AWS console wondering where to find a resource, or why a resource you're looking for doesn't appear.  &lt;/p&gt;

&lt;p&gt;And if you have ever used resources in more than one region it's reasonably likely that some of those times were resolved when you suddenly noticed up in the corner that the region you were on isn't the one you expected, or possibly the region the resource you were looking for is in a region you didn't expect.&lt;/p&gt;

&lt;p&gt;Even if most of your resources are in one region there are reasons why you might need to have some resources in another region. For instance, if you want to use an AWS Certificate Manager (ACM) certificate with CloudFront, &lt;a href="https://docs.aws.amazon.com/acm/latest/userguide/acm-regions.html" rel="noopener noreferrer"&gt;it has to be in us-east-1&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;When it really gets a little annoying is when you're not sure which region to look in -- now you're clicking from one region to the next, looking in each region for the resource.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter EC2 Global View
&lt;/h2&gt;

&lt;p&gt;AWS &lt;a href="https://aws.amazon.com/about-aws/whats-new/2021/09/amazon-ec2-global-view-console-regions/" rel="noopener noreferrer"&gt;announced EC2 Global View&lt;/a&gt;, a new interface for viewing your EC2 resources across all regions.&lt;/p&gt;

&lt;p&gt;Think of EC2 Global View as an alternate interface for viewing some of your EC2 resources. It's not part of the EC2 console, and it's not a new entry in the region menu, it's a whole separate interface.&lt;/p&gt;

&lt;p&gt;When you launch the interface, you'll see the Region Explorer:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnh1xhy8kfd5s1gdirhzf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnh1xhy8kfd5s1gdirhzf.png" alt="Global View: Region Explorer"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've numbered a few useful elements that we'll talk about.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Region Explorer (1)
&lt;/h3&gt;

&lt;p&gt;The region explorer will show you an overview of all the &lt;em&gt;supported resources&lt;/em&gt; that you have in all regions, and then a breakdown of those resources by region. Most of the elements you click on will take you to the Global Search with specific filters applied.  &lt;/p&gt;

&lt;p&gt;This is essentially a dashboard with roll-up information and quick links to areas of interest.  You can also use the search box in the region explorer to filter the regions that you're exploring.&lt;/p&gt;

&lt;h3&gt;
  
  
  Global Search (2)
&lt;/h3&gt;

&lt;p&gt;The Global Search shows you specific resources. You can switch to this view yourself and enter your own searches by resource id, tags or region:&lt;/p&gt;

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

&lt;p&gt;This is particularly useful if you have a good tagging strategy that might let you filter down to a very specific set of resources.&lt;/p&gt;

&lt;p&gt;It's also the view you'll come to when clicking links on the dashboard, which will populate the search with filters that match what you clicked on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search by Resource Type (3)
&lt;/h3&gt;

&lt;p&gt;If in the resource summary at the top of the region explorer, you click on any one of the resource types, you'll be shown a search view filtered by resources of that type.&lt;/p&gt;

&lt;p&gt;Here's a search for security groups:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fenkygkdeu8ruj0fefyn2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fenkygkdeu8ruj0fefyn2.png" alt="Search: Security Groups"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Search by Region (4)
&lt;/h3&gt;

&lt;p&gt;If you just want to see all the &lt;em&gt;supported resources&lt;/em&gt; in a region, simply click on the region in the first column of the region table, and you'll be taken to the Global Search with a region filter:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkguop8ac1pxm8ew7crsd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkguop8ac1pxm8ew7crsd.png" alt="Search: Region"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Alternately, go to the search view and start typing a region name.&lt;/p&gt;

&lt;h3&gt;
  
  
  Search by Region and Resource (5)
&lt;/h3&gt;

&lt;p&gt;If you want to search by both region and resource, simply click the cell at the intersection of the row corresponding to the region and the column corresponding to the resource. For instance, if I wanted to see volumes in ca-central-1:&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;p&gt;It's still early days. It's called "EC2 Global View", not "AWS Global View", so it's still limited to Instances, VPCs, Subnets, Security Groups, Volumes. I can't find my CloudFront ACM Certificate using Global View.&lt;/p&gt;

&lt;p&gt;It's also not super-discoverable. If you don't know that EC2 Global View exists as a separate console interface, then clicking in the region menu isn't going to give you any hints.&lt;/p&gt;

&lt;p&gt;Fortunately, you can share this article with everyone you know who uses AWS and they can discover it that way. &lt;/p&gt;

</description>
      <category>aws</category>
      <category>ec2</category>
      <category>cloud</category>
      <category>search</category>
    </item>
    <item>
      <title>Old AMIs on your AWS Account</title>
      <dc:creator>Geoffrey Wiseman</dc:creator>
      <pubDate>Tue, 05 Oct 2021 14:26:18 +0000</pubDate>
      <link>https://dev.to/aws-builders/old-amis-on-your-aws-account-2abd</link>
      <guid>https://dev.to/aws-builders/old-amis-on-your-aws-account-2abd</guid>
      <description>&lt;p&gt;If you're using Amazon Web Services (AWS), there's a pretty reasonable chance that you're also explicitly using an &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html"&gt;Amazon Machine Image&lt;/a&gt; (AMI) -- the format that AWS uses to make a virtual machine image. And if you've been using AWS for more than a few months, you might have some resources that were built with an old AMI. Maybe it was current when you created the instance or launch configuration, but has been aging since then.&lt;/p&gt;

&lt;p&gt;If you're using an old AMI and you haven't been patching it, or if you have been patching it but the release is no longer supported, your resources might be vulnerable to attack. &lt;a href="https://www.rapid7.com/fundamentals/patch-management/"&gt;Patch management&lt;/a&gt; is crucial to keeping a long-running server secure. Of course, just because an AMI is old doesn't mean it hasn't been patched. Amazon Linux and Ubuntu both have an option for a &lt;a href="https://en.wikipedia.org/wiki/Rolling_release"&gt;rolling release&lt;/a&gt; model that can keep an old AMI relatively secure. If you've invested a lot into devops, you might also have a continuous deployment pipeline that rebuilds &lt;a href="https://martinfowler.com/bliki/PhoenixServer.html"&gt;phoenix servers&lt;/a&gt; on a regular basis, which might include updating the AMI.&lt;/p&gt;

&lt;p&gt;You might also be using AMIs, but not explicitly. You might be implicitly using AMIs through &lt;a href="https://docs.aws.amazon.com/AmazonECS/latest/userguide/what-is-fargate.html"&gt;Fargate&lt;/a&gt; or &lt;a href="https://aws.amazon.com/lambda/"&gt;Lambda&lt;/a&gt;, for instance. Getting out of the business of configuring and patching server instances is part of the value proposition of the 'serverless' model. There are still servers and patches, but you, the AWS user, don't need to care about them. &lt;/p&gt;

&lt;p&gt;But if you're using AMIs and you're not sure that you've been keeping on top of the support timelines and the patch management, then it's probably also a good idea to consider how old those AMIs might be.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding Old AMIs
&lt;/h2&gt;

&lt;p&gt;If you are using AMIs, how can you find out if the AMIs you're using are current?  You can look at the AMI metadata:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;CreationDate&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;This tells you when the AMI was created.&lt;/li&gt;
&lt;li&gt;A creation date that isn't recent doesn't mean that image is unsupported or cannot be well-patched, but if you're still using an image created years ago, this might at least suggest that you need to take a closer look.&lt;/li&gt;
&lt;li&gt;If this image is still well-supported, you might still want to look at devops practices that allow you to rebuild old servers using more current images on a regular basis.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DeprecationTime&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;AWS &lt;a href="https://aws.amazon.com/about-aws/whats-new/2021/06/amazon-ec2-adds-new-ami-property-to-flag-outdated-amis/"&gt;recently added&lt;/a&gt; a new piece of metadata that allows the image creator / maintainer &lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ami-deprecate.html"&gt;to indicate&lt;/a&gt; the date/time on which the image is deprecated.&lt;/li&gt;
&lt;li&gt;This is more explicit than the creation date, but because it's new, there may well be many old images that are borderline abandoned and that don't have this date set.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, if you gather a list of all the AMIs that you're using, you can look at these two fields to get hints about which images need a closer examination. An image created a year or more ago, or marked as deprecated are both worth examining to see if they're well-patched and well-supported.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Tool
&lt;/h2&gt;

&lt;p&gt;If looking up all the AMIs in use on your account and checking the metadata sounds like a lot of work, then you're in luck. After reading about the new &lt;code&gt;DeprecationTime&lt;/code&gt; data point, I made a small JavaScript tool, &lt;code&gt;oldamis&lt;/code&gt; (&lt;a href="https://github.com/geoffreywiseman/oldamis"&gt;github&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/@codiform/oldamis"&gt;npm&lt;/a&gt;) that looks up AMIs and checks both of those data points for you. You can run it with &lt;code&gt;npx&lt;/code&gt; if you have NPM installed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❯ npx @codiform/oldamis
   ___    _       _        _      __  __   ___
  / _ \  | |   __| |      / \    |  \/  | |_ _|  ___
 | | | | | |  / _` |     / _ \   | |\/| |  | |  / __|
 | |_| | | | | (_| |    / ___ \  | |  | |  | |  \__ \
  \___/  |_|  \__,_|   /_/   \_\ |_|  |_| |___| |___/

ami ami-730ebd17 is old (created 2016-08-22T19:58:21.000-04:00), sources:
  - instance i-13e13eeb963a78ab9
ami ami-0cde1f5ee149df291 is ok, sources:
  - instance i-a3c31bb5ebbd4790d
  - instance i-11aff774c13d785ef
  - instance i-486d7a5e0171e6749
ami ami-0f1c5116668d961c3 is ok, sources:
  - instance i-8f434ca2c2c36dfb5
  - instance i-4b344522536719e4f
  - launch config demo-launch-config-2340234
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't a sophisticated tool -- it's a proof of concept. Organizations that want to monitor this sort of thing should probably be looking at monitoring or policy-as-code tools that can be configured to look at this and many, many other things that could go wrong with your AWS account. That said, if you're not sure how old the AMIs you're using are right now, and this tool makes your life easier, I'm happy to hear it.&lt;/p&gt;

&lt;p&gt;Just to be clear, &lt;code&gt;oldamis&lt;/code&gt; doesn't record information about your account, intercept data, or use your credentials in sneaky ways. The tool respects your privacy and its open-source, so you're welcome to dig into the code before running it, just to make sure.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it's made
&lt;/h2&gt;

&lt;p&gt;I made &lt;code&gt;oldamis&lt;/code&gt; with the &lt;a href="https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html"&gt;AWS JavaScript SDK v3&lt;/a&gt;. I've done a bunch of AWS automation using Python and Boto3 and wanted to try a different language and SDK for a change, and refresh my knowledge of publishing an NPM module.  There were a few hiccups, but all in all it worked well, and there's a good library for mocking API calls, which I've found to be somewhat important when you're writing a tool that is a thin layer over AWS API calls.&lt;/p&gt;

&lt;p&gt;As an example, once &lt;code&gt;oldamis&lt;/code&gt; has figured out which AMIs you might be using, it uses the DescribeImages api call to get the &lt;code&gt;DeprecationTime&lt;/code&gt; and &lt;code&gt;CreationDate&lt;/code&gt;:&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;getAmiDates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amis&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;command&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;DescribeImagesCommand&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ImageIds&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;amis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;IncludeDeprecated&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&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;ec2Client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;command&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;img&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="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ImageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DeprecationTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CreationDate&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nx"&gt;o&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ImageId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;DeprecationTime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;CreationDate&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;o&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;After getting the response it transforms the results into a smaller data structure for consumption by the CLI that looks like this:&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;"ami-730ebd17"&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;"CreationDate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2016-08-22T19:58:21.000-04:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"DeprecationTime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;There's more to it, but if you want to see more code samples, then I invite you to check out the GitHub repository. Contributions and feedback are welcome.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;oldami&lt;/code&gt; tool uses &lt;a href="https://github.com/chalk/chalk"&gt;chalk&lt;/a&gt; to colorize the output and &lt;a href="https://github.com/patorjk/figlet.js"&gt;figlet&lt;/a&gt; for the banner.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cli</category>
      <category>npm</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
