
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:
- The AWS CLI
- Terraform
- 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:
| Option | Account needed? | Best for |
|---|---|---|
| LocalStack (free Hobby token) | Yes, free sign-up | Widest service coverage, most tutorials and tooling |
| Moto (Python, open source) | No | Zero 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 --versionPart 1: Start LocalStack
Step 1: Get a free auth token
- Sign up at app.localstack.cloud with a Hobby (free, non-commercial) account.
- Copy your auth token from the web app.
- 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.sockStart it:
1
docker compose up -dBinding 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/healthYou'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 lsTry 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_REQUESTOption 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 = jsonAnd this to
~/.aws/credentials:1
2
3
[local]
aws_access_key_id = test
aws_secret_access_key = testNow target the local endpoint explicitly:
1
aws --profile local --endpoint-url http://localhost:4566 s3 lsUsing 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 lsPart 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-approveThen confirm the resources exist in the simulator:
1
2
aws --profile local s3 ls
aws --profile local sqs list-queuesWhen you're done:
1
terraform destroy -auto-approveShortcut: 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 applyPro 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 boto3Create
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.pyMake 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.pyUnset 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 5000Then 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 lsIn 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
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
- 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.
- 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.
- Keep real credentials out of your shell. Use dummy
testcredentials, a dedicatedlocalprofile, and never export production keys in the same terminal. - State disappears on restart unless you mount a persistent volume. That's usually a feature, because every test run starts clean.
- Pin your image version in CI. After the March 2026 change, an unpinned
latesttag or a missing token can suddenly break pipelines. Store the token as a CI secret. - Windows and WSL: if the CLI can't reach
localhost:4566from inside WSL, make sure Docker Desktop's WSL integration is enabled.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Container exits immediately | Missing or invalid LOCALSTACK_AUTH_TOKEN |
Could not connect to the endpoint URL | Container not running, or wrong port |
Terraform hangs on apply | Missing skip_* flags, so it's trying to reach real AWS |
| S3 errors about bucket DNS | Add s3_use_path_style = true |
InvalidClientTokenId | You 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.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article