AWS Builder Center
Build a Local AWS Environment: Test AWS CLI, Terraform & Python Without AWS Costs

Build a Local AWS Environment: Test AWS CLI, Terraform & Python Without AWS Costs

Using LocalStack and Moto to Safely Develop, Test, and Automate AWS Workloads on Your Laptop

Every AWS engineer has had the moment: you run terraform apply to "just test something", forget about it, and a month later the bill arrives. Or you want to practise the AWS CLI, but you're nervous about fat-fingering a command against a real account.
The fix is a local AWS simulator: a program on your laptop that speaks the AWS API. Your CLI, Terraform and boto3 scripts talk to it instead of the real cloud. Nothing is billed, nothing leaks, and you can wipe it and start over in seconds.
In this guide you'll set one up and point all three tools at it:
  1. The AWS CLI
  2. Terraform
  3. Python (boto3)

First, an important 2026 update

If you've read older tutorials, they all say: docker run localstack/localstack, done. That changed in March 2026.
LocalStack merged its free community image into a single unified image. Since version 2026.03.0, the container needs a LOCALSTACK_AUTH_TOKEN to start. A free non-commercial ("Hobby") account is available, so it's still free to use, but you now have to sign up and generate a token.
That leaves you with two good routes:
OptionAccount needed?Best for
LocalStack (free Hobby token)Yes, free sign-upWidest service coverage, most tutorials and tooling
Moto (Python, open source)NoZero sign-up, runs without Docker, great for Python tests
We'll do LocalStack first, then show Moto. The AWS CLI, Terraform and Python steps are almost identical for both. Only the endpoint URL changes.
Newer open-source emulators such as Floci and MiniStack have also appeared as no-token LocalStack replacements. They're young and I haven't covered them here, so check their docs before depending on them.

Prerequisites

  • Docker (Docker Desktop on Windows/macOS, or Docker Engine on Linux)
  • AWS CLI v2
  • Terraform 1.x
  • Python 3.9+ with pip
Verify everything quickly:
1
2
3
4
docker --version
aws --version
terraform version
python3 --version

Part 1: Start LocalStack

Step 1: Get a free auth token

  1. Sign up at app.localstack.cloud  with a Hobby (free, non-commercial) account.
  2. Copy your auth token from the web app.
  3. Export it in your shell:
1
export LOCALSTACK_AUTH_TOKEN="your-token-here"

Step 2: Run it with Docker Compose

Create a docker-compose.yml:
1
2
3
4
5
6
7
8
9
10
11
services:
localstack:
image: localstack/localstack:latest
container_name: localstack
ports:
- "127.0.0.1:4566:4566"
environment:
- LOCALSTACK_AUTH_TOKEN=${LOCALSTACK_AUTH_TOKEN}
volumes:
- ./localstack-data:/var/lib/localstack
- /var/run/docker.sock:/var/run/docker.sock
Start it:
1
docker compose up -d
Binding to 127.0.0.1 keeps the simulator reachable only from your own machine.

Step 3: Check it's alive

1
curl http://localhost:4566/_localstack/health
You'll get JSON listing the available services and their status. Port 4566 is the single "edge" endpoint that every service uses.

Part 2: Use the AWS CLI against the simulator

The CLI needs two things: some credentials (any dummy values work) and an --endpoint-url.

Option A: Plain AWS CLI

1
2
3
4
5
6
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1

aws --endpoint-url=http://localhost:4566 s3 mb s3://my-test-bucket
aws --endpoint-url=http://localhost:4566 s3 ls
Try a few more services:
1
2
3
4
5
6
7
8
9
# SQS
aws --endpoint-url=http://localhost:4566 sqs create-queue --queue-name demo-queue

# DynamoDB
aws --endpoint-url=http://localhost:4566 dynamodb create-table \
--table-name Users \
--attribute-definitions AttributeName=id,AttributeType=S \
--key-schema AttributeName=id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST

Option B: A dedicated profile (so you never mix up real and fake)

Add this to ~/.aws/config:
1
2
3
[profile local]
region = us-east-1
output = json
And this to ~/.aws/credentials:
1
2
3
[local]
aws_access_key_id = test
aws_secret_access_key = test
Now target the local endpoint explicitly:
1
aws --profile local --endpoint-url http://localhost:4566 s3 ls
Using a named profile is a good safety habit. If you forget --profile local, you'd hit your real account, so keep your real default credentials out of the shell while you practise. The AWS CLI config file does not support an endpoint_url setting; the endpoint must be set at runtime, usually with --endpoint-url or a matching environment variable in your shell.

Option C: awslocal

LocalStack ships a thin wrapper that adds the endpoint for you:
1
2
pip install awscli-local
awslocal s3 ls

Part 3: Use Terraform against the simulator

Terraform's AWS provider lets you override each service's endpoint. Create main.tf:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}

provider "aws" {
region = "us-east-1"
access_key = "test"
secret_key = "test"

# Skip checks that would try to reach real AWS
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true

# S3 path-style URLs work better locally
s3_use_path_style = true

endpoints {
s3 = "http://localhost:4566"
sqs = "http://localhost:4566"
dynamodb = "http://localhost:4566"
iam = "http://localhost:4566"
sts = "http://localhost:4566"
}
}

resource "aws_s3_bucket" "demo" {
bucket = "terraform-demo-bucket"
}

resource "aws_sqs_queue" "demo" {
name = "terraform-demo-queue"
}

resource "aws_dynamodb_table" "users" {
name = "Users"
billing_mode = "PAY_PER_REQUEST"
hash_key = "id"

attribute {
name = "id"
type = "S"
}
}
Run the usual workflow:
1
2
3
terraform init
terraform plan
terraform apply -auto-approve
Then confirm the resources exist in the simulator:
1
2
aws --profile local s3 ls
aws --profile local sqs list-queues
When you're done:
1
terraform destroy -auto-approve
Shortcut: LocalStack provides tflocal, a wrapper that injects the endpoint overrides automatically so you don't have to maintain the endpoints block:
1
2
pip install terraform-local
tflocal init && tflocal apply
Pro tip: put the endpoint config behind a variable (for example var.use_localstack) so the same code can target the simulator in dev and real AWS in production.

Part 4: Use Python (boto3) against the simulator

1
pip install boto3
Create demo.py:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import boto3

ENDPOINT = "http://localhost:4566"

def client(service):
return boto3.client(
service,
endpoint_url=ENDPOINT,
region_name="us-east-1",
aws_access_key_id="test",
aws_secret_access_key="test",
)

s3 = client("s3")
s3.create_bucket(Bucket="python-demo-bucket")
s3.put_object(Bucket="python-demo-bucket", Key="hello.txt", Body=b"Hello, local AWS!")

obj = s3.get_object(Bucket="python-demo-bucket", Key="hello.txt")
print(obj["Body"].read().decode())

sqs = client("sqs")
queue_url = sqs.create_queue(QueueName="python-demo-queue")["QueueUrl"]
sqs.send_message(QueueUrl=queue_url, MessageBody="test message")
msgs = sqs.receive_message(QueueUrl=queue_url)
print(msgs["Messages"][0]["Body"])
1
python demo.py

Make endpoints configurable

Don't hard-code the URL in real projects. Keep the endpoint in a single config value and pass it into the boto3 client instead of baking it into every call:
1
2
3
4
5
6
7
8
9
10
11
12
import os
import boto3

ENDPOINT = os.getenv("AWS_ENDPOINT_URL", "https://s3.amazonaws.com")

s3 = boto3.client(
"s3",
endpoint_url=ENDPOINT,
region_name="us-east-1",
aws_access_key_id=os.getenv("AWS_ACCESS_KEY_ID", "test"),
aws_secret_access_key=os.getenv("AWS_SECRET_ACCESS_KEY", "test"),
)
Then switch the environment at runtime:
1
2
export AWS_ENDPOINT_URL=http://localhost:4566
python your_existing_script.py
Unset it and the same script will use the default AWS endpoint. This is a safe pattern for local testing because the endpoint is managed explicitly instead of depending on undocumented client-wide defaults.

Part 5: The no-account alternative, Moto

If you don't want to sign up for anything, Moto is a mature, open-source AWS mock library for Python. It can run in two ways.

Mode 1: Standalone server (works with CLI, Terraform and Python)

1
2
pip install "moto[server]"
moto_server -p 5000
Then use http://localhost:5000 as your endpoint everywhere:
1
2
3
export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1
aws --endpoint-url=http://localhost:5000 s3 mb s3://moto-bucket
aws --endpoint-url=http://localhost:5000 s3 ls
In Terraform, change every endpoint in the endpoints block to http://localhost:5000. In boto3, set endpoint_url="http://localhost:5000".

Mode 2: In-process decorator (pure Python unit tests)

1
2
3
4
5
6
7
8
9
10
11
import boto3
from moto import mock_aws

@mock_aws
def test_upload():
s3 = boto3.client("s3", region_name="us-east-1")
s3.create_bucket(Bucket="unit-test-bucket")
s3.put_object(Bucket="unit-test-bucket", Key="a.txt", Body=b"hi")

body = s3.get_object(Bucket="unit-test-bucket", Key="a.txt")["Body"].read()
assert body == b"hi"
No Docker, no server, no network. Run it with pytest. This is the fastest feedback loop for testing Python code.

Which one should you pick?

  • Learning the AWS CLI or Terraform, or testing many services together: LocalStack (free Hobby token).
  • Python unit tests and CI with zero setup: Moto in-process.
  • No account, but you want a real endpoint for CLI and Terraform: Moto server mode.

Gotchas and best practices

  1. A simulator is not AWS. Behaviour, IAM enforcement, error messages and edge cases can differ. Treat local tests as a fast first check, then validate once in a real sandbox account before production.
  2. Not every service is covered equally. Check the emulator's service coverage before designing a test around a niche feature. Some advanced features may be missing or only available on paid tiers.
  3. Keep real credentials out of your shell. Use dummy test credentials, a dedicated local profile, and never export production keys in the same terminal.
  4. State disappears on restart unless you mount a persistent volume. That's usually a feature, because every test run starts clean.
  5. Pin your image version in CI. After the March 2026 change, an unpinned latest tag or a missing token can suddenly break pipelines. Store the token as a CI secret.
  6. Windows and WSL: if the CLI can't reach localhost:4566 from inside WSL, make sure Docker Desktop's WSL integration is enabled.

Troubleshooting

SymptomLikely cause
Container exits immediatelyMissing or invalid LOCALSTACK_AUTH_TOKEN
Could not connect to the endpoint URLContainer not running, or wrong port
Terraform hangs on applyMissing skip_* flags, so it's trying to reach real AWS
S3 errors about bucket DNSAdd s3_use_path_style = true
InvalidClientTokenIdYou forgot the endpoint and hit real AWS with fake keys

Wrapping up

With one container (or one pip install), you now have a throwaway AWS on your laptop. You can break it, rebuild it and automate against it without a cent of spend or a flicker of anxiety about your real account.
A sensible next step is to wire this into CI. Run your Terraform plan/apply and your pytest suite against the simulator on every pull request, and only promote to a real AWS account once everything is green.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article