Skip to content
Get Started for Free

CI Best Practices

Every CI system has its own configuration syntax, runner model, and feature set. Consult your CI provider’s own documentation for how to declare jobs, secrets, caches, and artifacts. This guide covers the parts that are the same everywhere: the LocalStack-specific commands you run, and the best practices for running a LocalStack job.

Whatever the provider, a CI job follows the same steps:

  1. Expose your CI Auth Token to the job as LOCALSTACK_AUTH_TOKEN.
  2. Install lstk and any tools your tests need, such as the Azure CLI or Terraform.
  3. Configure and start the emulator with lstk start.
  4. Deploy your infrastructure with the Azure CLI, Bicep, or Terraform.
  5. Run your tests.
  6. Collect the emulator logs as a build artifact.

Every LocalStack CI run needs a CI Auth Token, rather than a personal Developer Auth Token (see CI Environments). Store the token in your CI system as LOCALSTACK_AUTH_TOKEN. Every CI provider offers somewhere to keep sensitive values, and most distinguish secrets from plain environment variables. Secrets are masked in job logs and withheld from forked-repository builds. Never commit a token to your repository or paste it into a pipeline definition.

The lstk CLI passes the LOCALSTACK_AUTH_TOKEN value into the emulator container when it starts. There is no need to run lstk login, which works only in an interactive terminal.

Your job needs the lstk CLI, plus whichever Azure tooling your tests use. Many hosted runners already ship Docker and the Azure CLI, so check your runner image before adding an install step.

lstk is the recommended way to run and manage LocalStack. Install it in CI with npm or Homebrew:

Terminal window
npm install -g @localstack/lstk

See the lstk installation guide for all installation methods. In CI, pin the version, for example npm install -g @localstack/lstk@1.2.0, and bump it deliberately, so that a new lstk release doesn’t land in every pipeline at once. lstk also needs a working Docker daemon on the runner. It mounts the Docker socket into the emulator container, so the emulator can start the extra containers that some Azure services need.

lstk az runs your host az binary against the emulator, so the Azure CLI must be installed on the runner. Refer to the Azure CLI installation instructions if your runner doesn’t include it. The Azure CLI also compiles Bicep templates, and downloads the Bicep CLI the first time it needs it.

Install Terraform with your provider’s setup step where one exists (for example hashicorp/setup-terraform on GitHub Actions), or install it directly. Refer to the Terraform installation instructions for details, and to the Terraform guide for using it with the Azure emulator.

lstk reads its settings from a config.toml file. Commit a .lstk/config.toml to your repository, and both your developers and your CI jobs get the same emulator configuration, with no environment variables to duplicate across pipeline files. lstk picks up ./.lstk/config.toml automatically when it runs from the root of your source tree:

.lstk/config.toml
[[containers]]
type = "azure" # Start the Azure emulator
port = "4566"
env = ["ci"] # Apply the [env.ci] profile below
[env.ci]
MSSQL_ACCEPT_EULA = "Y" # Needed by Azure SQL Database

The type setting matters in CI. Without a config file, a non-interactive lstk start starts the default AWS emulator, so either commit this file or run lstk start --type azure. See the configuration reference for every available field.

lstk writes its own files next to the config file, such as its log and the Azure CLI configuration that lstk setup azure creates. When no system keyring is available, lstk login also stores your auth token there, in plain text, as auth-token. Commit only .lstk/config.toml, and ignore the rest:

.gitignore
.lstk/*
!.lstk/config.toml

Keep a single .lstk/config.toml for local development and CI where you can. However, if a CI job needs different settings, pass an alternative file with lstk --config ./ci/lstk.toml start.

The Azure emulator image is published with the latest and dev tags only, so you can’t pin a release with the tag field. With the default latest tag, lstk start checks for a newer image on every run. To keep your CI jobs on a build you’ve tested, copy the image to your own registry and point image at that copy:

[[containers]]
type = "azure"
image = "registry.example.com/localstack/localstack-azure:tested"
port = "4566"

See Custom container image for details.

Start LocalStack with a single command:

Terminal window
lstk start
Output
Pulling localstack/localstack-azure:latest...
Preparing LocalStack...
✔︎ Pulled localstack/localstack-azure:latest
Starting LocalStack...
✔︎ LocalStack is running (containerId: aade6d8cd6d0)
• Endpoint: localhost.localstack.cloud:4566
• Web app: https://app.localstack.cloud
> Tip: Check emulator status: lstk status

lstk start brings the emulator all the way to a ready state. It pulls the container image, starts the container, and returns only once the emulator is ready, so there is no need for a separate wait or health-check step. The Azure emulator validates your license itself when it starts. If startup fails, the command exits with a non-zero return code, causing your CI job to fail.

For machine-readable output, add the global --json flag. See structured output if your pipeline needs to inspect results programmatically.

Most CI pipelines create the resources their tests need by applying the same Infrastructure as Code they use for production.

After lstk start, run lstk setup azure once in the job. It prepares an Azure CLI configuration of its own that points at the emulator:

Terminal window
lstk setup azure
Output
Registering 'LocalStack' custom cloud...
Logging in with dummy service-principal credentials...
✔︎ Azure CLI integration ready. Run 'lstk az <command>' to talk to LocalStack.

lstk az then runs your az commands against the emulator, including Bicep deployments:

Terminal window
lstk az group create --name my-rg --location westeurope
lstk az deployment group create --resource-group my-rg --template-file main.bicep

lstk az leaves the runner’s own Azure CLI configuration untouched. See lstk az for details.

If your deployment scripts call plain az, or you use a tool that signs in through the Azure CLI, point the runner’s global Azure CLI configuration at the emulator instead:

Terminal window
lstk az start-interception

From then on, every az command on the runner targets the emulator. See Global interception for details.

Keep metadata_host and subscription_id out of your azurerm provider block, and point the provider at the emulator from the job’s environment instead. The provider reads metadata_host from ARM_METADATA_HOSTNAME and subscription_id from ARM_SUBSCRIPTION_ID, so CI tests the same configuration that your CD pipeline deploys to real Azure.

Unless you configure other credentials, the azurerm provider signs in through the Azure CLI. Run lstk az start-interception before Terraform, then run your usual commands:

Terminal window
lstk az start-interception
export ARM_METADATA_HOSTNAME="azure.localhost.localstack.cloud:4566"
export ARM_SUBSCRIPTION_ID="00000000-0000-0000-0000-000000000000"
terraform init
terraform apply -auto-approve

The Terraform guide shows a complete example that sets both values in the provider block instead, which is fine for local experiments.

Once the emulator is running and your resources are deployed, run your test suite. Tests that drive the Azure CLI work through lstk az, or through plain az after lstk az start-interception. For Python code that uses the Azure SDK, see the Python guide.

Export the emulator logs before the job ends, then store them as a build artifact:

Terminal window
lstk logs --verbose > localstack.log

Run this step even when the tests fail, so you capture the logs regardless of success or failure. To make failures easier to diagnose in the first place, set DEBUG = "1" in your CI environment profile.

Was this page helpful?