<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Michael J</title>
    <description>The latest articles on Forem by Michael J (@kiwimike).</description>
    <link>https://forem.com/kiwimike</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3543212%2F60df04f8-9b8d-4e19-bbf2-87933d75d864.jpg</url>
      <title>Forem: Michael J</title>
      <link>https://forem.com/kiwimike</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/kiwimike"/>
    <language>en</language>
    <item>
      <title>Enabling Faster Development Feedback with LocalStack: Lessons from the Software Development Lifecycle</title>
      <dc:creator>Michael J</dc:creator>
      <pubDate>Fri, 10 Oct 2025 12:26:23 +0000</pubDate>
      <link>https://forem.com/kiwimike/enabling-faster-development-feedback-with-localstack-lessons-from-the-software-development-m2l</link>
      <guid>https://forem.com/kiwimike/enabling-faster-development-feedback-with-localstack-lessons-from-the-software-development-m2l</guid>
      <description>&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; A story about AWS app development frustrations and how I'd implement better testing practices in my SDLC for faster feedback (and a better experience). &lt;/p&gt;

&lt;p&gt;A few years ago, I was briefly introduced to LocalStack while working as a DevOps Engineer on a platform built with several AWS-native microservices. Before my time, during the initial build, the team had developed a testing suite using LocalStack. It was out of date, and we never updated it, but in hindsight, I wish we had. &lt;/p&gt;

&lt;h2&gt;
  
  
  Development Challenges
&lt;/h2&gt;

&lt;p&gt;The platform, built on AWS, used a mixture of serverless and managed services orchestrated through events and queues. &lt;/p&gt;

&lt;p&gt;Naturally, this meant when making functional changes to the platform, we had to modify the payloads being exchanged between components. As a &lt;em&gt;severely fat-fingered individual&lt;/em&gt;, every change of this nature required lots of testing. &lt;/p&gt;

&lt;p&gt;Fortunately, we had sandbox environments that we could use to test our application during development. &lt;/p&gt;

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

&lt;p&gt;Unfortunately, we only had two sandbox environments, which caused several issues: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only two engineers could actively develop and test their branches at once. &lt;/li&gt;
&lt;li&gt;A "dirty engineer" (usually me) who didn't clean up resources after testing would cause problems for whoever came next. &lt;/li&gt;
&lt;li&gt;We had to wait for requests to pass through the system and dig through logs and traces to uncover behavioural events.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;As a result, much of our development time was spent submitting payloads and waiting for requests to process, only to discover the issue was a simple typo or an incorrect IAM policy.&lt;/p&gt;

&lt;p&gt;This ad hoc approach to testing resulted in extended timelines and quite frankly, could just be annoying to deal with. &lt;/p&gt;

&lt;p&gt;To mitigate bugs appearing in releases, we used what I would imagine is a pretty standard CI/CD pipeline. &lt;/p&gt;

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

&lt;p&gt;Our CI/CD pipeline at least validated our deployments, but even with our pipeline, we were slow to release features because fundamentally, we relied &lt;strong&gt;too much&lt;/strong&gt; on our pipeline - which had to build and deploy our end-to-end application into a live environment so we could test it. &lt;/p&gt;

&lt;h2&gt;
  
  
  The importance of fast feedback
&lt;/h2&gt;

&lt;p&gt;I used to (and sometimes still do) fall into the trap of jankily getting a new feature over the line, which often resulted in more debugging time overall. &lt;/p&gt;

&lt;p&gt;The further we get from our own machine, the longer it will take us to get feedback, essentially: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On our own machine, it's fast to debug and uncover issues we can quickly action. &lt;/li&gt;
&lt;li&gt;In our CI pipeline, the runner executing our pipeline needs to load, dependencies need to be installed, and we need to wait for the results to become available for us. &lt;/li&gt;
&lt;li&gt;Lastly, in a live environment, we need to actively view our observability systems, looking at logs and traces to identify what the issue might be. &lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The more we can do on our local device, the faster the feedback and quicker (and less annoying) our development experience will be. &lt;/p&gt;

&lt;h2&gt;
  
  
  Where LocalStack fits in
&lt;/h2&gt;

&lt;p&gt;LocalStack is an AWS, Snowflake (and soon Azure) service emulator that runs locally in a container, in a CI/CD pipeline, and now, also in LocalStack's own cloud.  It's a commercial product, but the free edition works great in most cases. &lt;/p&gt;

&lt;p&gt;Rather than creating a LocalStack tutorial, if you haven't used it before, you can reference these resources to learn more: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.localstack.cloud/aws/?__hstc=108988063.1aef873bfe1f17cd547a5bd1d3aaf44c.1751623177192.1759480723999.1760078283706.11&amp;amp;__hssc=108988063.1.1760078283706&amp;amp;__hsfp=1216282399" rel="noopener noreferrer"&gt;LocalStack for AWS Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/localstack-samples" rel="noopener noreferrer"&gt;LocalStack Samples&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;TLDR;&lt;/strong&gt; When you start LocalStack, it exposes native cloud APIs, so any tool that works with native APIs should work with LocalStack.  It doesn't just emulate the API, but also the service behaviour, allowing you to deploy emulated cloud-native resources (such as SQS Queues or Event Buses) that you can use with your application in the same way you'd use the live service, all locally. &lt;/p&gt;

&lt;p&gt;Environments can be ephemeral, allowing you to deploy completely clean each time and validate 100% that your application can deploy end-to-end, which is useful for developing multi-tenant applications you may have to re-deploy. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzs6gkx2qsesp51je0gcq.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzs6gkx2qsesp51je0gcq.gif" alt="Running LocalStack Deployment and Tests" width="760" height="614"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, it's important to differentiate LocalStack from AWS services themselves.  I've read stories from those who have developed their whole application end-to-end using LocalStack before proceeding to deploy directly to AWS. &lt;/p&gt;

&lt;p&gt;LocalStack is ultimately an emulator, so use LocalStack for initial testing but not end-to-end testing.  You still need to validate how your application performs in a live environment.  &lt;/p&gt;

&lt;h2&gt;
  
  
  How I would have built LocalStack into the software development lifecycle (SDLC)
&lt;/h2&gt;

&lt;p&gt;We can make the best use of LocalStack in various areas of our SDLC, but not everywhere - we still need to involve AWS. &lt;/p&gt;

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

&lt;p&gt;LocalStack works best on the left (earlier in development).  While writing code, it's incredibly useful to spin up my LocalStack instance and validate that my Lambda Function not only deploys correctly, but also properly integrates with other AWS services, that it sends and receives the proper data to the SQS Queue, and that it can interact with my Database correctly. &lt;/p&gt;

&lt;p&gt;However, we should be writing structured tests along with our features that validate that our code works as expected. You can use any test framework you'd like, since we're essentially interacting with native AWS services (just locally). &lt;/p&gt;

&lt;p&gt;Building guardrails with our tests is just as important and from an SDLC perspective, there are a few ways we can do that: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pre-Commit&lt;/li&gt;
&lt;li&gt;CI/CD Pipelines&lt;/li&gt;
&lt;li&gt;Human feedback (via observability data collected)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Pre-Commit: Testing locally
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://pre-commit.com/#intro" rel="noopener noreferrer"&gt;Pre-Commit&lt;/a&gt; is a python package we can install.  It hooks into our &lt;code&gt;git commit&lt;/code&gt; command, so that before our staged files are committed, the hooks we've configured execute on them. &lt;/p&gt;

&lt;p&gt;It’s an important step in the SDLC because it runs tests locally on our machine, ensuring fast feedback.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pre-commit.com/#new-hooks" rel="noopener noreferrer"&gt;We can build hooks in many languages&lt;/a&gt;, including plain scripts that can run a series of our tests and return successfully to pass, alternatively, we can build more complex and reusable hooks using a fully-fledged language. &lt;/p&gt;

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

&lt;p&gt;That means we can create a Pre-Commit hook that does the following: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Starts our LocalStack container&lt;/li&gt;
&lt;li&gt;Deploys our service's AWS resources to LocalStack using IaC&lt;/li&gt;
&lt;li&gt;Executes our AWS integration tests for our service&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; file could look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .pre-commit-config.yaml &lt;/span&gt;
&lt;span class="na"&gt;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="c1"&gt;# startup LocalStack - this is optional if you already have it running. &lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;localstack-start&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start LocalStack&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt; &lt;span class="c1"&gt;# our hook starts with `bash`&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-c&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;localstack start -d&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;# don't pass the staged files as arguments&lt;/span&gt;
        &lt;span class="na"&gt;always_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;# always run this step&lt;/span&gt;

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;cdk-deploy&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy AWS Resources&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt; 
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-c&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;set -e&lt;/span&gt;
            &lt;span class="s"&gt;export CDK_DISABLE_CONTEXT_SAVE=1&lt;/span&gt;
            &lt;span class="s"&gt;export AWS_CDK_DISABLE_VERSION_CHECK=1&lt;/span&gt;
            &lt;span class="s"&gt;cdk deploy&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; 
        &lt;span class="na"&gt;always_run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; 

  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;local&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;integration-tests&lt;/span&gt;
        &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Python Tests&lt;/span&gt;
        &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python&lt;/span&gt; &lt;span class="c1"&gt;# our hook starts with `python`&lt;/span&gt;
        &lt;span class="na"&gt;language&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;system&lt;/span&gt;
        &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-m&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pytest&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;tests/test_lambda_unit.py&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;-v&lt;/span&gt;
        &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;^(lambda/|tests/).*\.py$&lt;/span&gt; &lt;span class="c1"&gt;# only run if these files are staged&lt;/span&gt;
        &lt;span class="na"&gt;pass_filenames&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
        &lt;span class="na"&gt;require_serial&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="c1"&gt;#only run after cdk-synth has finished&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;You should also add other common checks to your pre-commit setup, such as security scans, linting, formatting checks, and the like.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI/CD: Testing in-pipeline
&lt;/h3&gt;

&lt;p&gt;Continuous integration and delivery are pretty standard these days. To make matters easier, you can execute pre-commit hooks in your CI/CD pipeline using the &lt;code&gt;pre-commit run --run-all-files&lt;/code&gt; command. &lt;/p&gt;

&lt;p&gt;I've had the best developer experience when having a pipeline I can run from my development branch to deploy to a sandbox environment, so I'd still recommend incorporating that functionality into your development pipelines. The perk of this approach is that we can enable additional observability services such as CloudWatch logging, application tracing, and OpenSearch, all of which are incredibly useful when debugging.  Of course, sometimes nothing beats debugging in a live environment, when you can just search through logs in your OpenSearch instance. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqkfmwjtjqsko0bxgpdh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feqkfmwjtjqsko0bxgpdh.png" alt="CI/CD Pipeline Flow Chart" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For those who use GitHub Actions, you can make use of the &lt;a href="https://docs.localstack.cloud/aws/integrations/continuous-integration/github-actions/" rel="noopener noreferrer"&gt;LocalStack Action&lt;/a&gt;.  If you don't use GitHub, there are also examples for other CI platforms such as GitLab, CodeBuild and BitBucket. &lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/tests.yml&lt;/span&gt;

&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# I can manually run the pipeline&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;AWS_DEFAULT_REGION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us-east-1&lt;/span&gt; &lt;span class="c1"&gt;# We can use any region, as it's just emulated&lt;/span&gt;
  &lt;span class="na"&gt;AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt; &lt;span class="c1"&gt;# We can use any credentials&lt;/span&gt;
  &lt;span class="na"&gt;AWS_SECRET_ACCESS_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;localstack-tests&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment &amp;amp; Integration Tests&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;linting&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;sast-scan&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt; &lt;span class="c1"&gt;# run your linting and SAST stages first. &lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start LocalStack&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LocalStack/setup-localstack@v0.2.2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;image-tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;latest'&lt;/span&gt;
          &lt;span class="na"&gt;install-awslocal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;true'&lt;/span&gt;
          &lt;span class="na"&gt;configuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;DEBUG=1&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Python&lt;/span&gt;
        &lt;span class="c1"&gt;# ...text omitted&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Set up Node.js&lt;/span&gt;
        &lt;span class="c1"&gt;# ...text omitted&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install runtime and test dependencies&lt;/span&gt;
        &lt;span class="c1"&gt;# ...text omitted&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bootstrap &amp;amp; Deploy CDK to LocalStack&lt;/span&gt;
        &lt;span class="c1"&gt;# ...text omitted&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Integration Tests&lt;/span&gt;
        &lt;span class="c1"&gt;# ...text omitted&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Destroy stack (cleanup)&lt;/span&gt;
        &lt;span class="c1"&gt;# ...text omitted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enabling debug in LocalStack is important so you can review any issues your application might have with the emulated AWS services. &lt;/p&gt;

&lt;h2&gt;
  
  
  Some final advice
&lt;/h2&gt;

&lt;p&gt;Building your own SDLC requires understanding how you and your team prefer to develop, the nature of the application itself, and the budget and time you have available.&lt;/p&gt;

&lt;p&gt;My recommendations for testing with LocalStack are: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Before trying to test with LocalStack, use it. Read the &lt;a href="https://docs.localstack.cloud/aws/getting-started/quickstart/" rel="noopener noreferrer"&gt;Quickstart Guide&lt;/a&gt; and deploy some resources. &lt;/li&gt;
&lt;li&gt;Use LocalStack to test integrations between your application and AWS services.  For example, testing how your application interacts with SQS or  DynamoDB. &lt;/li&gt;
&lt;li&gt;Make your tests simple but meaningful - overcomplexity can result in avoiding testing altogether! Focus on validating practical behaviour. &lt;/li&gt;
&lt;li&gt;Make an effort to keep your tests and LocalStack up-to-date, as new features are released in LocalStack and your application, it's important to ensure your tests continue to work. &lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Finally, LocalStack is a commercial product.  The free version covers practically every integration service, but if you use EKS, or want to test out your full end-to-end application entirely, then LocalStack Ultimate might be the way forward.&lt;/p&gt;

&lt;p&gt;These days, whenever I’m developing on AWS, I validate my logic locally using LocalStack. It lets me spin up a PoC in minutes and, more importantly, build a robust testing process for myself and others - catching issues early and saving hours of development time (and probably pain).&lt;/p&gt;

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

&lt;p&gt;If you've used LocalStack before and have any advice, or if you've learned any interesting lessons yourself, drop them in the comments! &lt;/p&gt;

&lt;p&gt;&lt;em&gt;If you'd like any examples of the strategies above, I've created a &lt;a href="https://github.com/Mike-RJ/aws-cdnz-2025/tree/main" rel="noopener noreferrer"&gt;example repository&lt;/a&gt; that you can reference.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>aws</category>
      <category>productivity</category>
      <category>localstack</category>
    </item>
  </channel>
</rss>
