<?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>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>
