<?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: Lucas Tonelli</title>
    <description>The latest articles on DEV Community by Lucas Tonelli (@lucastonelli).</description>
    <link>https://dev.to/lucastonelli</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4117551%2F558f3bf2-550f-4113-a250-9d2b9dfdbb5b.jpeg</url>
      <title>DEV Community: Lucas Tonelli</title>
      <link>https://dev.to/lucastonelli</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lucastonelli"/>
    <language>en</language>
    <item>
      <title>Storing and managing secrets cheaply and safely</title>
      <dc:creator>Lucas Tonelli</dc:creator>
      <pubDate>Tue, 27 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/lucastonelli/storing-and-managing-secrets-cheaply-and-safely-em3</link>
      <guid>https://dev.to/lucastonelli/storing-and-managing-secrets-cheaply-and-safely-em3</guid>
      <description>&lt;p&gt;Every application has its secrets (database passwords, API keys, tokens, etc.) and they need to live somewhere that isn't a plaintext file committed to Git (please, don't). For those familiar with AWS, the first thought that comes to mind is to go with Secrets Manager to manage them, and it works great, except for the price per secret per month. Later down the road, it quietly turns into a line item nobody remembers approving once you multiply it across services, environments and regions. The good news is: for most secrets, there's a cheaper place to put them that's just as safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storing secrets with SSM Parameter Store
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html" rel="noopener noreferrer"&gt;Parameter Store&lt;/a&gt; is part of AWS Systems Manager and it holds configuration values for your applications. For anything sensitive, you use the &lt;code&gt;SecureString&lt;/code&gt; type, which encrypts the value at rest using &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/secure-string-parameter-kms-encryption.html" rel="noopener noreferrer"&gt;AWS KMS&lt;/a&gt;; for everything else you may either use &lt;code&gt;String&lt;/code&gt; or &lt;code&gt;StringList&lt;/code&gt;. The best part is that the &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html#parameter-store-tiers-introduction" rel="noopener noreferrer"&gt;standard tier is free&lt;/a&gt; and you can get a decent amount of storage and other benefits without paying a cent.&lt;/p&gt;

&lt;p&gt;Writing a secret is a one-liner and if you don't point it at a specific key, it uses the AWS managed key &lt;code&gt;aws/ssm&lt;/code&gt; that Systems Manager creates for you, which costs nothing to have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ssm put-parameter &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"/myapp/prod/my-secrets"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s2"&gt;"super-secret-value"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--type&lt;/span&gt; SecureString
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reading it back requires &lt;code&gt;--with-decryption&lt;/code&gt;, otherwise you'll just get the ciphertext staring back at you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ssm get-parameter &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"/myapp/prod/my-secrets"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--with-decryption&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--query&lt;/span&gt; Parameter.Value &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--output&lt;/span&gt; text
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A nice bonus is that the path-like naming isn't just for looks. You can scope IAM permissions to a prefix like &lt;code&gt;/myapp/prod/*&lt;/code&gt;, so a service only ever sees the secrets it's supposed to and nothing from other paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Comparing with AWS Secrets Manager
&lt;/h2&gt;

&lt;p&gt;Both services encrypt with KMS, so from a security standpoint they're basically the same. What you're actually paying for with &lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html" rel="noopener noreferrer"&gt;Secrets Manager&lt;/a&gt; is a set of features Parameter Store doesn't have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_lambda.html" rel="noopener noreferrer"&gt;Lambda rotations&lt;/a&gt; included in the price;&lt;/li&gt;
&lt;li&gt;Native integration with RDS, Redshift and DocumentDB, so rotating database credentials is mostly wired up already (&lt;a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/rotate-secrets_managed.html" rel="noopener noreferrer"&gt;managed rotation&lt;/a&gt;);&lt;/li&gt;
&lt;li&gt;Cross-region replication and easier cross-account sharing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Breaking it down, if you have credentials that auto-rotate through Secrets Manager, leave them there. Rebuilding rotation yourself to save a few cents might not be a good use of your time. However, a large chunk of secrets might never rotate at all - static API keys, third-party tokens, webhook signing secrets and so on. Those are often wasting money with Secrets Manager.&lt;/p&gt;

&lt;p&gt;For those, Parameter Store solves the problem for free. And if you ever bump into the size limit or need parameter policies, &lt;a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-advanced-parameters.html" rel="noopener noreferrer"&gt;advanced parameters&lt;/a&gt; are still way cheaper than Secrets Manager, in case someone tries to guilt you about it. However, just like every other AWS' product, &lt;a href="https://docs.aws.amazon.com/general/latest/gr/asm.html" rel="noopener noreferrer"&gt;Secrets Manager&lt;/a&gt; and &lt;a href="https://docs.aws.amazon.com/general/latest/gr/ssm.html" rel="noopener noreferrer"&gt;Parameter Store&lt;/a&gt; both have quotas and limits, so check and compare before committing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Watch out for the KMS decryption bill
&lt;/h2&gt;

&lt;p&gt;Now, there's always a caveat (another one). Parameter Store doesn't charge you to store a &lt;code&gt;SecureString&lt;/code&gt;, but the decryption and other requests, &lt;a href="https://aws.amazon.com/kms/pricing/" rel="noopener noreferrer"&gt;aren't free&lt;/a&gt;. Despite having a free tier, every single &lt;code&gt;get-parameter --with-decryption&lt;/code&gt; triggers a KMS &lt;code&gt;Decrypt&lt;/code&gt; call, and if you take a look at the pricing, it's nothing, right?!&lt;/p&gt;

&lt;p&gt;Now, picture a Lambda that pulls fifteen separate secrets on every request, receiving traffic all day, plus a couple of CI pipelines fetching the same values on every build. Suddenly you're making KMS requests like you're trying to break a record. Also, if those parameters are on the advanced tier, you're also paying just to store them separately.&lt;/p&gt;

&lt;p&gt;So, if applicable, group related secrets into a single &lt;code&gt;SecureString&lt;/code&gt; parameter as a JSON string. One parameter, one decrypt call per fetch, one thing to manage.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ssm put-parameter &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"/myapp/prod/my-secrets"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--type&lt;/span&gt; SecureString &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--value&lt;/span&gt; &lt;span class="s1"&gt;'{"db_password":"...","api_key":"...","jwt_secret":"..."}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then you fetch it once and let the application pull out whatever it needs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RAW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;aws ssm get-parameter &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"/myapp/prod/my-secrets"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--with-decryption&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--query&lt;/span&gt; Parameter.Value &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--output&lt;/span&gt; text&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$RAW&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.db_password'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also, since I don't want you creating new problems: both Parameter Store tiers have a storage cap, so don't try to cram every secret you own into one blob - group them by what's actually fetched together, usually per service or per environment. Adding to that, since the whole value is one parameter, changing a single field means rewriting the entire JSON, so anything that needs independent rotation might be better off on its own. It's a trade-off, not a silver bullet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Reach for SSM Parameter Store as your cheap-and-safe default and group the values into a JSON (beware of the storage limit) so KMS doesn't nickel-and-dime you on every request. Keep Secrets Manager for the handful of secrets where rotation and native integrations actually earn their price. Same security, a fraction of the bill.&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;This article was created with the help of AI because, while I'm good at being a software engineer (I hope so), I suck at being a writer. On the other hand, the silly jokes and wordplay are completely mine because I'm really good at those. Also, the content comes from things I've done or learned, has been verified and is edited to be straight to the point, since AI loves to stall, but I hate it.&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>security</category>
      <category>cybersecurity</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Optimizing AWS S3 costs</title>
      <dc:creator>Lucas Tonelli</dc:creator>
      <pubDate>Tue, 20 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/lucastonelli/optimizing-aws-s3-costs-4kgh</link>
      <guid>https://dev.to/lucastonelli/optimizing-aws-s3-costs-4kgh</guid>
      <description>&lt;p&gt;It's easy to create an S3 bucket, let objects pile up and watch everything sit comfortably in the Standard class whether it's accessed a thousand times a day or has been its fourth abandonment anniversary. However, AWS gives you the tools to figure out where your money is actually going (they'll surely charge for it lol) and when used properly, they'll show you where to start cutting costs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the big picture with S3 Storage Lens
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage_lens.html" rel="noopener noreferrer"&gt;S3 Storage Lens&lt;/a&gt; is an organization-wide dashboard for your storage. It aggregates usage and activity across all your accounts, regions and buckets into a single place. So instead of clicking through buckets one by one to answer questions such as "Which buckets hold most of my data?" you get a unified view with filters to precisely help you with that.&lt;/p&gt;

&lt;p&gt;The good thing is that every account gets a &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage_lens_metrics_glossary.html" rel="noopener noreferrer"&gt;default dashboard with free metrics&lt;/a&gt;, no setup required. The free tier gives you several usage metrics with 14 days of retention, which is already enough to spot some offenders. For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Buckets with a huge amount of noncurrent object versions (versioning enabled, lifecycle rules forgotten);&lt;/li&gt;
&lt;li&gt;Incomplete multipart upload bytes sitting there for months;&lt;/li&gt;
&lt;li&gt;Buckets that are 100% Standard storage even though nothing suggests they need to be.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to go deeper, there are the &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage_lens_basics_metrics_recommendations.html#storage_lens_basics_metrics_selection" rel="noopener noreferrer"&gt;advanced metrics and recommendations&lt;/a&gt;: activity metrics (requests, bytes downloaded, etc.), advanced cost optimization and data protection metrics, prefix-level aggregation, CloudWatch publishing and 15 months of retention. These are paid though, and we'll get back to that in a bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding access patterns with S3 Storage Class Analysis
&lt;/h2&gt;

&lt;p&gt;Storage Lens gives you the big picture while &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/analytics-storage-class.html" rel="noopener noreferrer"&gt;Storage Class Analysis&lt;/a&gt; tells you your storage's access patterns. You enable it per bucket (optionally filtered by prefix or tags) and it starts watching your objects' access patterns: how much data is stored, how much is retrieved, and how that changes through time.&lt;/p&gt;

&lt;p&gt;After observing for at least 30 days, it gives you a recommendation on when objects become "infrequently accessed", which is your cue to create a &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lifecycle-mgmt.html" rel="noopener noreferrer"&gt;lifecycle rule&lt;/a&gt; transitioning them to Standard-IA. You can also export the results daily as CSV to a bucket and slice them however you like (QuickSight, a spreadsheet, whatever makes you happy).&lt;/p&gt;

&lt;p&gt;It's worth noting that the analysis only makes recommendations for transitions from Standard to Standard-IA, and it's also a paid feature, charged per million objects monitored. If your access patterns are unpredictable and you just want to stop thinking about the problem, &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/userguide/intelligent-tiering.html" rel="noopener noreferrer"&gt;S3 Intelligent-Tiering&lt;/a&gt; might be a simpler answer than analyzing anything at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't let these tools become your new costs
&lt;/h2&gt;

&lt;p&gt;Both Storage Lens' advanced metrics and Storage Class Analysis charge you per million objects monitored, per month. Turn them on across your accounts indiscriminately, where you might have billions of small objects (such as build outputs), and congratulations, you've just blown your AWS bill.&lt;/p&gt;

&lt;p&gt;The way to avoid that is to do the heavy lifting with the free tier first:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start with the free Storage Lens metrics. They offer pretty decent data for you to build a shortlist of offenders (try using the &lt;a href="https://en.wikipedia.org/wiki/Pareto_principle" rel="noopener noreferrer"&gt;Pareto principle&lt;/a&gt; here);&lt;/li&gt;
&lt;li&gt;Enable the paid tools only on the shortlist and estimate the costs before turning them on (you may do that with &lt;a href="https://calculator.aws/#/addService" rel="noopener noreferrer"&gt;AWS Pricing Calculator&lt;/a&gt;). Turn on Storage Class Analysis for specific buckets, and use prefix or tag filters to narrow it down further instead of monitoring every single object. Use the same logic for Storage Lens advanced metrics: scope the dashboard to the accounts or buckets you're actually studying;&lt;/li&gt;
&lt;li&gt;Turn them off when you're done. Once you have gathered enough information to define what you need to do and your lifecycle rules are in place, disable the analysis. It's a diagnostic tool, not an Amazon Prime subscription. The same goes for Storage Lens.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Use free Storage Lens metrics (advanced ones, if needed) to find the suspects, Storage Class Analysis on the few buckets that justify it, then apply lifecycle rules to act on what you learned and switch the paid monitoring off. That's it, see you!&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;This article was created with the help of AI because, while I'm good at being a software engineer (I hope so), I suck at being a writer. On the other hand, the silly jokes and wordplay are completely mine because I'm really good at those. Also, the content comes from things I've done or learned, has been verified and is edited to be straight to the point, since AI loves to stall, but I hate it.&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>s3</category>
      <category>aws</category>
      <category>infrastructure</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Caching your API at the edge with fine-grained purging controls</title>
      <dc:creator>Lucas Tonelli</dc:creator>
      <pubDate>Tue, 13 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/lucastonelli/caching-your-api-at-the-edge-with-fine-grained-purging-controls-5m9</link>
      <guid>https://dev.to/lucastonelli/caching-your-api-at-the-edge-with-fine-grained-purging-controls-5m9</guid>
      <description>&lt;p&gt;Caching API responses at the edge sounds great until you hit the invalidation problem. A TTL that's too short and your origin keeps getting hammered, too long and your users see stale data for who knows how long. If what you're looking for is to cache aggressively and purge surgically, the moment something changes and only what changed, there's a solution for that. Spoiler alert: the usual AWS suspects don't really compete here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting your API behind Fastly
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://www.fastly.com/documentation/guides/full-site-delivery/getting-started-with-full-site-delivery/" rel="noopener noreferrer"&gt;setup&lt;/a&gt; is the usual reverse proxy dance: you create a Fastly service, point it to your API as the origin (or "host"), and &lt;a href="https://www.fastly.com/documentation/guides/getting-started/domains/working-with-domains/working-with-cname-records-and-your-dns-provider/" rel="noopener noreferrer"&gt;move your DNS to Fastly&lt;/a&gt;. From that moment on, every request goes through their &lt;a href="https://www.fastly.com/documentation/guides/getting-started/concepts/using-fastlys-global-pop-network/" rel="noopener noreferrer"&gt;points of presence&lt;/a&gt; (POP) and cacheable responses get stored close to your users.&lt;/p&gt;

&lt;p&gt;Now that caching is set up, what about the "fine-grained purging controls"? This is where the &lt;a href="https://docs.fastly.com/en/guides/working-with-surrogate-keys" rel="noopener noreferrer"&gt;Surrogate-Key header&lt;/a&gt; comes in. Your origin can tag any response with one or more keys, space-separated, and Fastly will index the cached object under all of them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="k"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1.1&lt;/span&gt; &lt;span class="m"&gt;200&lt;/span&gt; &lt;span class="ne"&gt;OK&lt;/span&gt;
&lt;span class="na"&gt;Content-Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;application/json&lt;/span&gt;
&lt;span class="na"&gt;Surrogate-Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;products product-42 category-shoes&lt;/span&gt;
&lt;span class="s"&gt;// Surrogate-Control works as well&lt;/span&gt;
&lt;span class="na"&gt;Cache-Control&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;max-age=86400&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of it as labeling your cache. That response for &lt;code&gt;GET /products/42&lt;/code&gt; is now reachable by "everything product-related", "this specific product" and "everything in the shoes category" at the same time. Also, Fastly strips the header before sending the response to the client, so your users never see your labeling scheme. One key can be attached to thousands of objects, and one object can carry multiple keys, allowing for surgical purging later on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Purging by keys
&lt;/h3&gt;

&lt;p&gt;When product 42 gets updated, you don't need to guess which URLs contain it, or nuke the entire cache out of despair. You just &lt;a href="https://www.fastly.com/documentation/reference/api/purging/" rel="noopener noreferrer"&gt;purge the key&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="s2"&gt;"https://api.fastly.com/service/&lt;/span&gt;&lt;span class="nv"&gt;$SERVICE_ID&lt;/span&gt;&lt;span class="s2"&gt;/purge/product-42"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Fastly-Key: &lt;/span&gt;&lt;span class="nv"&gt;$FASTLY_API_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every cached object tagged with &lt;code&gt;product-42&lt;/code&gt; is invalidated across the whole network, usually in a few milliseconds. The listing page, the detail endpoint, that search response that happened to include it, all gone in one call while everything else stays warm.&lt;/p&gt;

&lt;p&gt;There's a catch, though (of course). URL-based purges on Fastly work &lt;a href="https://www.fastly.com/documentation/guides/full-site-delivery/purging/authenticating-api-purge-requests/" rel="noopener noreferrer"&gt;out of the box without authentication&lt;/a&gt;, so anyone who knows your endpoints could purge them and turn your cache into swiss cheese (rude, but possible). But don't worry, the fix is adding the header &lt;code&gt;http.Fastly-Purge-Requires-Auth: "1"&lt;/code&gt; change at your service configuration.&lt;/p&gt;

&lt;p&gt;With authenticated purging enabled, every purge request must carry a valid API token with purge permissions, otherwise it gets a 401 and goes home empty-handed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customizing further with VCL scripts
&lt;/h3&gt;

&lt;p&gt;If the defaults aren't enough, Fastly lets you write &lt;a href="https://www.fastly.com/documentation/guides/vcl/custom/" rel="noopener noreferrer"&gt;custom VCL&lt;/a&gt; (Varnish Configuration Language) scripts that run at the edge, on every request and you can invoke them in hooks of the &lt;a href="https://www.fastly.com/documentation/reference/vcl/request-lifecycle/" rel="noopener noreferrer"&gt;request lifecycle&lt;/a&gt;: &lt;code&gt;vcl_recv&lt;/code&gt; when the request arrives, &lt;code&gt;vcl_fetch&lt;/code&gt; when the origin responds, &lt;code&gt;vcl_deliver&lt;/code&gt; before sending it back, and so on.&lt;/p&gt;

&lt;p&gt;That opens a lot of possibilities. For instance, you can normalize query strings so &lt;code&gt;?a=1&amp;amp;b=2&lt;/code&gt; and &lt;code&gt;?b=2&amp;amp;a=1&lt;/code&gt; hit the same cache object, vary the cache by an API version header or even generate surrogate keys at the edge when your origin can't be bothered:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight vcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Script injected in the "hash" hook to cache products based on page number&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;req.url.path &lt;span class="o"&gt;~&lt;/span&gt; &lt;span class="s2"&gt;"^/products"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;set&lt;/span&gt; req.hash &lt;span class="o"&gt;+=&lt;/span&gt; querystring.filter_except&lt;span class="p"&gt;(&lt;/span&gt;querystring.sort&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.url&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="s2"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;set&lt;/span&gt; req.hash &lt;span class="o"&gt;+=&lt;/span&gt; querystring.remove&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;req.url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;set&lt;/span&gt; req.hash &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nv"&gt;req.http.host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set&lt;/span&gt; req.hash &lt;span class="o"&gt;+=&lt;/span&gt; req.vcl.generation&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;hash&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also cache things Fastly wouldn't cache by default (like &lt;a href="https://www.fastly.com/documentation/solutions/examples/caching-post-requests/" rel="noopener noreferrer"&gt;POST bodies&lt;/a&gt; for search endpoints, if you know what you're doing) or strip cookies that would otherwise bust your cache. Basically, if the behavior you want isn't a checkbox, it's probably a few lines of VCL away.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about CloudFront and API Gateway caching?
&lt;/h2&gt;

&lt;p&gt;If your stack is on AWS, the natural instinct is to reach for &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/ConfiguringCaching.html" rel="noopener noreferrer"&gt;CloudFront&lt;/a&gt; or the &lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-caching.html" rel="noopener noreferrer"&gt;API Gateway cache&lt;/a&gt;. They'll cache your responses just fine, but the control story is a different beast.&lt;/p&gt;

&lt;p&gt;CloudFront has no equivalent to surrogate keys. Its &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html" rel="noopener noreferrer"&gt;Invalidations&lt;/a&gt; are path-based, so to invalidate "everything related to product 42" you either track every URL that contains it (good luck with query strings) or throw a &lt;code&gt;/*&lt;/code&gt; wildcard and wipe way more than you needed. On top of that, invalidations take minutes to propagate instead of milliseconds, and past the free monthly quota, &lt;a href="https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/PayingForInvalidation.html" rel="noopener noreferrer"&gt;each path costs money&lt;/a&gt;. Purging as part of your write path, the way you'd casually do with Fastly, just isn't how it was designed.&lt;/p&gt;

&lt;p&gt;API Gateway is even more restrictive: the cache is a fixed-size instance you pay for hourly, and invalidation is either flushing the entire stage cache or letting clients send &lt;code&gt;Cache-Control: max-age=0&lt;/code&gt; to invalidate a single entry, which you then have to control with an IAM policy so random clients can't do it. No tags, no keys, no grouping, no edge programmability. It works for "cache this GET for 5 minutes", and that's about it.&lt;/p&gt;

&lt;p&gt;None of this means these services are bad, they're just built for different use cases. If precise, event-driven invalidation is a requirement, they'll fight you the whole way. It's also worth mentioning that Fastly offers a free tier for their services, so you'll likely be able to set up a few projects without spending anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;Fine-grained purging is what turns edge caching from a risky optimization into something you can lean on: tag responses with &lt;code&gt;Surrogate-Key&lt;/code&gt;, purge those keys from your write path the instant data changes, lock purging down with &lt;code&gt;Fastly-Purge-Requires-Auth&lt;/code&gt;, and reach for VCL when the defaults fall short. Your origin gets quieter, your users get fast and fresh responses, and you stop choosing between the two.&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;This article was created with the help of AI because, while I'm good at being a software engineer (I hope so), I suck at being a writer. On the other hand, the silly jokes and wordplay are completely mine because I'm really good at those. Also, the content comes from things I've done or learned, has been verified and is edited to be straight to the point, since AI loves to stall, but I hate it.&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>cloud</category>
      <category>api</category>
      <category>infrastructure</category>
    </item>
    <item>
      <title>Easily secure your non-prod environments from external users for free</title>
      <dc:creator>Lucas Tonelli</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/lucastonelli/easily-secure-your-non-prod-environments-from-external-users-for-free-2668</link>
      <guid>https://dev.to/lucastonelli/easily-secure-your-non-prod-environments-from-external-users-for-free-2668</guid>
      <description>&lt;p&gt;If you have a private domain sitting on the public internet, chances are anyone can reach it. Search engines index it, curious people poke at it, and bots scan it all day long. The usual fixes are a VPN (complex and annoying to maintain), IP allowlists (break the moment someone works from a coffee shop), or basic auth (everyone shares the same password forever). However, the good thing is that there's another way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cloudflare Access
&lt;/h2&gt;

&lt;p&gt;The idea is simple, your DNS record for the environment must be proxied through Cloudflare (the orange cloud), so every request passes through their edge. Then you create a self-hosted &lt;a href="https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/" rel="noopener noreferrer"&gt;Cloudflare Access application&lt;/a&gt; pointing to that domain, and attach a &lt;a href="https://developers.cloudflare.com/cloudflare-one/access-controls/policies/" rel="noopener noreferrer"&gt;policy&lt;/a&gt; to it, for example, allowing only emails ending in &lt;code&gt;@mycompany.com&lt;/code&gt; or a specific list of addresses.&lt;/p&gt;

&lt;p&gt;From that point on, anyone hitting the gated address gets redirected to a login page, with an input to enter an email. Once the email is submitted, Cloudflare sends a one-time PIN to it, if it matches the conditions in the policy, then the user gets a session cookie and goes through. If the email doesn't match, the user never touches your servers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code example
&lt;/h3&gt;

&lt;p&gt;You can click this together on the Cloudflare dashboard, but you can also do it through Terraform with two resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"cloudflare_zero_trust_access_application"&lt;/span&gt; &lt;span class="s2"&gt;"development"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;account_id&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cloudflare_account_id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"development.${var.project_name}"&lt;/span&gt;
  &lt;span class="nx"&gt;domain&lt;/span&gt;                     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"development.${var.apex_domain_name}"&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;                       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"self_hosted"&lt;/span&gt;
  &lt;span class="nx"&gt;session_duration&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"12h"&lt;/span&gt;
  &lt;span class="nx"&gt;http_only_cookie_attribute&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="nx"&gt;app_launcher_visible&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;policies&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cloudflare_zero_trust_access_policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
    &lt;span class="nx"&gt;precedence&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"cloudflare_zero_trust_access_policy"&lt;/span&gt; &lt;span class="s2"&gt;"default"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;account_id&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cloudflare_account_id&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Default"&lt;/span&gt;
  &lt;span class="nx"&gt;decision&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"allow"&lt;/span&gt;
  &lt;span class="nx"&gt;session_duration&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"24h"&lt;/span&gt;
  &lt;span class="nx"&gt;include&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
    &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;default_email&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Worth noting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;session_duration&lt;/code&gt; controls how long the cookie is valid, so people are not logging in on every request;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;http_only_cookie_attribute&lt;/code&gt; keeps the session cookie away from JavaScript;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app_launcher_visible&lt;/code&gt; hides the app from Cloudflare's app launcher page, since this is just an internal environment;
The policy is separate from the application, so you can reuse the same &lt;code&gt;default&lt;/code&gt; policy across multiple apps.
Do you need to protect staging and QA too? Duplicate the application resource (or use &lt;code&gt;for_each&lt;/code&gt;) pointing to each subdomain, and reference the same policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Costs
&lt;/h2&gt;

&lt;p&gt;This is the best part, it's free. &lt;a href="https://www.cloudflare.com/plans/zero-trust-services/" rel="noopener noreferrer"&gt;Cloudflare's Zero Trust free tier covers up to 50 users&lt;/a&gt;, which is plenty for protecting internal environments of most teams.&lt;/p&gt;

&lt;p&gt;There is only one catch (of course there would be a catch). The free &lt;a href="https://developers.cloudflare.com/ssl/edge-certificates/universal-ssl/" rel="noopener noreferrer"&gt;Universal SSL&lt;/a&gt; certificate &lt;a href="https://developers.cloudflare.com/ssl/edge-certificates/universal-ssl/limitations/#full-setup" rel="noopener noreferrer"&gt;covers the root domain and one level of subdomain&lt;/a&gt;. So &lt;code&gt;development.myproject.com&lt;/code&gt;, &lt;code&gt;potatochips.myproject.com&lt;/code&gt; and &lt;code&gt;whatever.myproject.com&lt;/code&gt; are all fine. But if you go one level deeper, like &lt;code&gt;api.development.myproject.com&lt;/code&gt;, that's a second-level subdomain and the free certificate won't cover it, so you'd need Cloudflare's paid &lt;a href="https://developers.cloudflare.com/ssl/edge-certificates/advanced-certificate-manager/" rel="noopener noreferrer"&gt;Advanced Certificate Manager&lt;/a&gt; for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;For such a simple setup and a proxied DNS record, your non-prod environments stop being public. No VPN, no shared passwords, no drastic changes, just an email check at the edge. It's one of those rare cases where it seems too good to be true, but it actually is pretty good.&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;This article was created with the help of AI because, while I'm good at being a software engineer (I hope so), I suck at being a writer. On the other hand, the silly jokes and wordplay are completely mine because I'm really good at those. Also, the content comes from things I've done or learned, has been verified and is edited to be straight to the point, since AI loves to stall, but I hate it.&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>infrastructure</category>
      <category>security</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Reduce SQS costs with these three strategies</title>
      <dc:creator>Lucas Tonelli</dc:creator>
      <pubDate>Tue, 29 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/lucastonelli/reduce-sqs-costs-with-these-three-strategies-b8f</link>
      <guid>https://dev.to/lucastonelli/reduce-sqs-costs-with-these-three-strategies-b8f</guid>
      <description>&lt;p&gt;SQS is a very popular and cheap service from AWS which seems pretty inoffensive to your monthly bill. However, it charges per request, not per message, and that detail is where most of the waste hides. Things like racking up old and unused queues with live consumers, using FIFO indiscriminately and being careless about polling will surely make AWS very happy and your pockets lighter. The good news is: there are a few ways which will help you save some money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enable Long Polling
&lt;/h2&gt;

&lt;p&gt;By default, SQS uses &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-short-and-long-polling.html" rel="noopener noreferrer"&gt;short polling&lt;/a&gt;: your consumer calls &lt;code&gt;ReceiveMessage&lt;/code&gt;, SQS answers immediately (even with an empty response), and your consumer calls again. Every one of those calls are billed, including the empty ones. A worker polling in a tight loop can easily generate millions of requests per month just to hear "nothing new".&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/best-practices-setting-up-long-polling.html" rel="noopener noreferrer"&gt;long polling&lt;/a&gt;, SQS holds the connection open for up to 20 seconds and only responds when there's a message (or when the wait time expires). One call every 20 seconds instead of dozens per second - same behavior for your application, but a fraction of the requests on your bill. Also, enabling it is a one-liner at the queue level:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws sqs set-queue-attributes &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--queue-url&lt;/span&gt; &lt;span class="nv"&gt;$QUEUE_URL&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--attributes&lt;/span&gt; &lt;span class="nv"&gt;ReceiveMessageWaitTimeSeconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or per request, by passing it on the ReceiveMessage API request. There might be no downsides for most workloads, but be sure to check if it fits your use case.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prefer standard queues to FIFO
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-fifo-queues.html" rel="noopener noreferrer"&gt;FIFO queues&lt;/a&gt; are convenient, exactly-once processing and strict ordering out of the box, but that convenience costs more per request than standard queues, and it also caps your throughput (you'll also have to pay more to increase it). The thing is, for a lot of systems you can get the same guarantees in the application layer.&lt;/p&gt;

&lt;h3&gt;
  
  
  Handling duplicates
&lt;/h3&gt;

&lt;p&gt;Standard queues offer &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/standard-queues-at-least-once-delivery.html" rel="noopener noreferrer"&gt;at-least-once delivery&lt;/a&gt;, so the same message may arrive more than once. Instead of paying FIFO to deduplicate for you, make your consumer idempotent. Give each message a unique ID and record it when processed (a unique constraint in your database or a key in Redis). If the same ID shows up again, skip it (or do something else, I don't know).&lt;/p&gt;

&lt;h3&gt;
  
  
  Ensuring ordering
&lt;/h3&gt;

&lt;p&gt;Standard queues also &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-queue-types.html" rel="noopener noreferrer"&gt;don't guarantee ordering&lt;/a&gt;, but in practice you might not need the global ordering from the queue. To circumvent that issue, add a timestamp (or version number) to each message at the producer, generated by the application. Then, when consuming, compare it against the last message processed: if the incoming message is older, skip it. An out-of-order message becomes a no-op instead of a bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migrating from FIFO to standard
&lt;/h3&gt;

&lt;p&gt;This is the sad part, where there's no configuration, nothing simple or even magical to do that. To &lt;a href="https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/FIFO-queues-moving.html" rel="noopener noreferrer"&gt;go back to using standard queues&lt;/a&gt; you need to create another queue and migrate your application to the newly created one. If you have a lot, like an enormous amount of FIFO queues, and you need to migrate them, I feel for you, I really do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delete unused queues
&lt;/h2&gt;

&lt;p&gt;This one sounds obvious, but it's pretty easy to forget a queue created for a feature that was deprecated, an experiment, an old environment and, along with it, a consumer somewhere still polling it 24/7. Since empty receives are billed like any other request, you're literally paying for API calls that will never return anything.&lt;/p&gt;

&lt;p&gt;A quick way to find these zombies is to look at the &lt;code&gt;NumberOfEmptyReceives&lt;/code&gt; metric in CloudWatch. A queue with a high count of empty receives and zero messages sent is a strong candidate for deletion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;None of these strategies require redesigning your architecture. Long polling is a configuration change, idempotency and timestamps are small additions to your application code, and cleaning up unused queues is just housekeeping. So, keep up with these tips, and make every SQS request count.&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;This article was created with the help of AI because, while I'm good at being a software engineer (I hope so), I suck at being a writer. On the other hand, the silly jokes and wordplay are completely mine because I'm really good at those. Also, the content comes from things I've done or learned, has been verified and is edited to be straight to the point, since AI loves to stall, but I hate it.&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>infrastructure</category>
      <category>aws</category>
      <category>sqs</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Host your async app without spending a dime using AWS Lambda</title>
      <dc:creator>Lucas Tonelli</dc:creator>
      <pubDate>Tue, 22 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/lucastonelli/host-your-async-app-without-spending-a-dime-using-aws-lambda-490k</link>
      <guid>https://dev.to/lucastonelli/host-your-async-app-without-spending-a-dime-using-aws-lambda-490k</guid>
      <description>&lt;p&gt;If you have an async app, an internal tool, side project or PoC, there's no reason to pay for a machine that's awake all day for a job that takes so little time. That's where AWS Lambda comes in, charging you only when your function gets executed, with a generous free tier. So how do you take what you have, written with FastAPI, Express, Django, Fastify or whatever, and drop it into Lambda without a lot of refactoring?&lt;/p&gt;

&lt;h2&gt;
  
  
  AWS Lambda Web Adapter
&lt;/h2&gt;

&lt;p&gt;AWS maintains an open source project called the &lt;a href="https://github.com/aws/aws-lambda-web-adapter" rel="noopener noreferrer"&gt;Lambda Web Adapter&lt;/a&gt; which is a small proxy that sits in front of your app. Lambda invokes the adapter, the adapter translates the event into a plain HTTP request, forwards it to your app and translates the response back into whatever shape Lambda expects. Your app has no idea it's running inside Lambda. It just sees an HTTP request on a port, like it always did.&lt;/p&gt;

&lt;p&gt;This means you don't have to refactor anything (not this time at least), you just add the adapter (copying the binary into your image if you're using a container or adding a Lambda Layer) and point it to your app's port. Here's a minimal Dockerfile example for a NodeJS app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;public.ecr.aws/lambda/nodejs:24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;base&lt;/span&gt;

&lt;span class="c"&gt;# This is required to make the adapter work as expected&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; []&lt;/span&gt;

&lt;span class="c"&gt;# Copy the Lambda Web Adapter binary from its distributed image&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=public.ecr.aws/awsguru/aws-lambda-adapter:1.0.1 /lambda-adapter /opt/extensions/lambda-adapter&lt;/span&gt;

&lt;span class="c"&gt;# Tells the adapter which port to listen&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; AWS_LWA_PORT=3000&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package.json ${LAMBDA_TASK_ROOT}/package.json&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package-lock.json ${LAMBDA_TASK_ROOT}/package-lock.json&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--production&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "app.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The adapter handles that translation transparently, including support for streaming responses if your app relies on them. For instance, this very website is running inside a Lambda function (yup).&lt;/p&gt;

&lt;h2&gt;
  
  
  Packaging your app
&lt;/h2&gt;

&lt;p&gt;I've given an example using Docker images, but there are a few other options for you to package your app, and they all fit different needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  ZIP
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-function-zip.html" rel="noopener noreferrer"&gt;The simplest option&lt;/a&gt;. You zip your code and dependencies, upload it directly (through the console or CLI), and Lambda runs it on its own managed runtime. There's no image to build or registry to manage, but you're capped at 250 MB unzipped (50 MB zipped), and native dependencies need to match Lambda's underlying OS.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-self-managed-storage.html" rel="noopener noreferrer"&gt;Practically the same&lt;/a&gt; as the ZIP approach, but it can accept packages greater than 50 MB zipped. You push the zip to a bucket and point Lambda at that object instead of uploading it inline. Same 250 MB limit applies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Docker
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/images-create.html" rel="noopener noreferrer"&gt;The option to reach for&lt;/a&gt; once you outgrow the other limits, need OS-level dependencies that aren't in the standard runtimes, or just want your Lambda environment to look like everything else you already run in containers. Also, the images can go up to 10 GB.&lt;/p&gt;

&lt;p&gt;Once the application is packaged and the Lambda function is deployed, you need something in front of it that speaks HTTPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting an endpoint: Lambda Function URL
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html" rel="noopener noreferrer"&gt;Function URL&lt;/a&gt;s are the fastest option. Every Lambda function can have a dedicated HTTPS endpoint enabled with a single setting, no extra resources to create. You get a generated URL and requests are forwarded straight to your function. If you just need an endpoint for a webhook, an internal service, or a quick prototype, this is usually all you need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting an endpoint: AWS HTTP API Gateway
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api.html" rel="noopener noreferrer"&gt;HTTP API Gateway&lt;/a&gt; makes sense once you need more than a bare endpoint: custom domains, request throttling, built-in authorizers, etc. It's a small extra piece of infrastructure to manage, but it's also cheap and pay-per-request, so it doesn't break the "free unless you're actually getting traffic" (or a DDoS attack lol) model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worth knowing before you commit
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html" rel="noopener noreferrer"&gt;Lambda has a hard execution limit&lt;/a&gt;, so this isn't a fit for long-lived connections;&lt;/li&gt;
&lt;li&gt;Cold starts are real, especially with container images, but they shouldn't be such a problem in a function with more frequent traffic. So, keep the image lean and consider using &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/provisioned-concurrency.html" rel="noopener noreferrer"&gt;provisioned concurrency&lt;/a&gt; if it matters. However, this is basically spinning up a server and paying for the time it's up;&lt;/li&gt;
&lt;li&gt;A single Lambda function &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-concurrency.html" rel="noopener noreferrer"&gt;reserved concurrency&lt;/a&gt; serves a single request, so plan your concurrency accordingly, to avoid being surprised by throttling;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;For various async apps out there, some idle, others with spiky traffic or without persistent connections, this setup gets you a real production app with an endpoint to receive requests that'll cost you nothing until someone actually uses it! So, what are you going to build with it?!&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;This article was created with the help of AI because, while I'm good at being a software engineer (I hope so), I suck at being a writer. On the other hand, the silly jokes and wordplay are completely mine because I'm really good at those. Also, the content comes from things I've done or learned, has been verified and is edited to be straight to the point, since AI loves to stall, but I hate it.&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>lambda</category>
      <category>infrastructure</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Avoid these mistakes when using Terraform with S3 backend</title>
      <dc:creator>Lucas Tonelli</dc:creator>
      <pubDate>Tue, 15 Sep 2026 13:00:00 +0000</pubDate>
      <link>https://dev.to/lucastonelli/avoid-these-mistakes-when-using-terraform-with-s3-backend-58h9</link>
      <guid>https://dev.to/lucastonelli/avoid-these-mistakes-when-using-terraform-with-s3-backend-58h9</guid>
      <description>&lt;p&gt;Using an S3 backend for Terraform is a popular choice because it's cheap, reliable and widely understood. However, it's pretty easy to configure the bare minimum and move on, forgetting three small things that, in the long run, will very likely make someone's life miserable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Table for locking
&lt;/h2&gt;

&lt;p&gt;Without a lock table, two engineers (or two CI/CD pipelines) can run &lt;code&gt;terraform apply&lt;/code&gt; at the same time, both reading the same state, and then both writing conflicting results back. It can corrupt your state file silently, leaving your real infrastructure, and the recorded state, permanently out of sync and you rethinking your life choices.&lt;/p&gt;

&lt;h3&gt;
  
  
  Adding a DynamoDB lock table
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_dynamodb_table"&lt;/span&gt; &lt;span class="s2"&gt;"tf_lock"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-state-lock"&lt;/span&gt;
  &lt;span class="nx"&gt;billing_mode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"PAY_PER_REQUEST"&lt;/span&gt;
  &lt;span class="nx"&gt;hash_key&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LockID"&lt;/span&gt;

  &lt;span class="nx"&gt;attribute&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"LockID"&lt;/span&gt;
    &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"S"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"s3"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;bucket&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-tf-state"&lt;/span&gt;
    &lt;span class="nx"&gt;key&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform.tfstate"&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"us-east-1"&lt;/span&gt;
    &lt;span class="nx"&gt;dynamodb_table&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"terraform-state-lock"&lt;/span&gt;
    &lt;span class="nx"&gt;encrypt&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Versioning S3 bucket
&lt;/h2&gt;

&lt;p&gt;Your state file is not code, it's a live record of everything Terraform believes about your infrastructure (that's a beautiful quote). Without versioning, a bad apply, a manual edit or an accidental &lt;code&gt;terraform state rm&lt;/code&gt; overwrites the state with no way back. Enabling versioning keeps a full history of every state revision, turning recovery from a crisis into a five-minute rollback instead of a life of suffering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enabling versioning on the state bucket
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"tf_state"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-tf-state"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_versioning"&lt;/span&gt; &lt;span class="s2"&gt;"tf_state"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tf_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;versioning_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_lifecycle_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"tf_state"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tf_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"expire-old-versions"&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&lt;/span&gt;
    &lt;span class="nx"&gt;noncurrent_version_expiration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;noncurrent_days&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;90&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;It's worth having a lifecycle rule to keep storage costs in check, in cases where there are a lot of changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enabling cross-region bucket replication
&lt;/h2&gt;

&lt;p&gt;While it's rare for an AWS region to go down, when it does, your S3 bucket goes with it. If your state file is only in one region, you can't run Terraform at all during the outage, even to fix the very infrastructure that's affected. Cross-region replication keeps a live copy of every state version in a second region so your operations don't have a single geographic point of failure. Also, for the paranoid ones, if you want to add to the already humongous durability of S3 objects, having another region is a way to do that.&lt;/p&gt;

&lt;h3&gt;
  
  
  Replicating state to a second region
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"tf_state_replica"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"my-tf-state-replica"&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;us_west_2&lt;/span&gt; &lt;span class="c1"&gt;# Secondary region&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_versioning"&lt;/span&gt; &lt;span class="s2"&gt;"tf_state_replica"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tf_state_replica&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;provider&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;us_west_2&lt;/span&gt;

  &lt;span class="nx"&gt;versioning_configuration&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket_replication_configuration"&lt;/span&gt; &lt;span class="s2"&gt;"tf_state"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tf_state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;role&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_iam_role&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;

  &lt;span class="nx"&gt;rule&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;id&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"replicate-state"&lt;/span&gt;
    &lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Enabled"&lt;/span&gt;
    &lt;span class="nx"&gt;destination&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;bucket&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_s3_bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tf_state_replica&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
      &lt;span class="nx"&gt;storage_class&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"STANDARD"&lt;/span&gt;
      &lt;span class="nx"&gt;replica_kms_key_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;aws_kms_key&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;replica&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;arn&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="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;Replication requires versioning enabled on both source and destination buckets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;p&gt;These three settings are pretty quick to set up and they help mitigating a class of incidents that make software engineers wake up at 3 am. So, ensure you're locking, versioning and replicating your state - sleep tight.&lt;/p&gt;




&lt;p&gt;&lt;small&gt;&lt;small&gt;&lt;small&gt;This article was created with the help of AI because, while I'm good at being a software engineer (I hope so), I suck at being a writer. On the other hand, the silly jokes and wordplay are completely mine because I'm really good at those. Also, the content comes from things I've done or learned, has been verified and is edited to be straight to the point, since AI loves to stall, but I hate it.&lt;/small&gt;&lt;/small&gt;&lt;/small&gt;&lt;/p&gt;

</description>
      <category>s3</category>
      <category>aws</category>
      <category>terraform</category>
      <category>infrastructure</category>
    </item>
  </channel>
</rss>
