Test Terraform Locally with LocalStack: S3, SQS and DynamoDB Without an AWS Account

· 2 min read · AWS Tutorials

Why LocalStack #

Testing infrastructure code against a real AWS account costs money and time, and a mistake can have consequences. LocalStack runs a local emulation of many AWS services in one Docker container. You can run terraform apply against it in seconds, in a laptop or in CI, and throw everything away.

Start LocalStack #

docker run -d --name localstack -p 127.0.0.1:4566:4566 localstack/localstack
curl -s http://localhost:4566/_localstack/health | jq

The health endpoint lists the services. Everything is available at http://localhost:4566 (the edge endpoint). See jq for the filter.

With Docker Compose:

compose.yaml
services:
  localstack:
    image: localstack/localstack
    ports:
      - "127.0.0.1:4566:4566"
    volumes:
      - localstack-data:/var/lib/localstack
volumes:
  localstack-data:

Use the AWS CLI #

Any credentials work. Set dummy values and the endpoint:

export AWS_ACCESS_KEY_ID=test AWS_SECRET_ACCESS_KEY=test AWS_DEFAULT_REGION=us-east-1
aws --endpoint-url=http://localhost:4566 s3 mb s3://demo-bucket
aws --endpoint-url=http://localhost:4566 s3 ls
aws --endpoint-url=http://localhost:4566 sqs create-queue --queue-name jobs

The awslocal wrapper (pip install awscli-local) adds the endpoint for you.

Point Terraform to LocalStack #

main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region                      = "us-east-1"
  access_key                  = "test"
  secret_key                  = "test"
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true
  s3_use_path_style           = true

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

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

resource "aws_sqs_queue" "jobs" {
  name = "jobs"
}

resource "aws_dynamodb_table" "orders" {
  name         = "orders"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "id"

  attribute {
    name = "id"
    type = "S"
  }
}
terraform init
terraform apply -auto-approve
aws --endpoint-url=http://localhost:4566 dynamodb list-tables

s3_use_path_style is needed because the emulator has no wildcard DNS for bucket.localhost. Add one endpoints line for each service you use. The same configuration works with OpenTofu.

One configuration for local and real AWS #

Use a variable so the endpoints appear only in tests:

variable "use_localstack" {
  type    = bool
  default = false
}

provider "aws" {
  region                      = "us-east-1"
  access_key                  = var.use_localstack ? "test" : null
  secret_key                  = var.use_localstack ? "test" : null
  skip_credentials_validation = var.use_localstack
  skip_requesting_account_id  = var.use_localstack
  s3_use_path_style           = var.use_localstack

  dynamic "endpoints" {
    for_each = var.use_localstack ? [1] : []
    content {
      s3       = "http://localhost:4566"
      sqs      = "http://localhost:4566"
      dynamodb = "http://localhost:4566"
    }
  }
}

Run terraform apply -var use_localstack=true for local tests. See dynamic blocks.

Test with terraform test #

The native test framework (Terraform testing) can run against LocalStack in CI: start the container, run terraform test, stop the container. Pair it with the scanners in Terraform security scanning.

Reset and clean up #

terraform destroy -auto-approve
docker rm -f localstack

Removing the container discards all the emulated data unless you mounted a volume for persistence.

Limits #

  • The free edition covers the common services; some advanced services need a paid plan.
  • IAM policies are not enforced by default.
  • Always run a final test in a sandbox AWS account.

#Localstack #AWS #Terraform #Docker