Test Terraform with LocalStack Go Client

In modern cloud engineering, Infrastructure as Code (IaC) is the gold standard for managing resources. Terraform has emerged as a leader in this space, allowing teams to define and provision infrastructure using a declarative configuration language. However, a significant challenge remains: how do you test your Terraform configurations efficiently without spinning up costly cloud resources and slowing down your development feedback loop? The answer lies in local cloud emulation. This guide provides a comprehensive walkthrough on how to leverage the powerful combination of Terraform LocalStack and the Go programming language to create a robust, local testing framework for your AWS infrastructure. This approach enables rapid, cost-effective integration testing, ensuring your code is solid before it ever touches a production environment.

Why Bother with Local Cloud Development?

The traditional “code, push, and pray” approach to infrastructure changes is fraught with risk and inefficiency. Testing against live AWS environments incurs costs, is slow, and can lead to resource conflicts between developers. A local cloud development strategy, centered around tools like LocalStack, addresses these pain points directly.

  • Cost Efficiency: By emulating AWS services on your local machine, you eliminate the need to pay for development or staging resources. This is especially beneficial when testing services that can be expensive, like multi-AZ RDS instances or EKS clusters.
  • Speed and Agility: Local feedback loops are orders of magnitude faster. Instead of waiting several minutes for a deployment pipeline to provision resources in the cloud, you can apply and test changes in seconds. This dramatically accelerates development and debugging.
  • Offline Capability: Develop and test your infrastructure configurations even without an internet connection. This is perfect for remote work or travel.
  • Isolated Environments: Each developer can run their own isolated stack, preventing the “it works on my machine” problem and eliminating conflicts over shared development resources.
  • Enhanced CI/CD Pipelines: Integrating local testing into your continuous integration (CI) pipeline allows you to catch errors early. You can run a full suite of integration tests against a LocalStack instance for every pull request, ensuring a higher degree of confidence before merging.

Setting Up Your Development Environment

Before we dive into the code, we need to set up our toolkit. This involves installing the necessary CLIs and getting LocalStack up and running with Docker.

Installing Core Tools

Ensure you have the following tools installed on your system. Most can be installed easily with package managers like Homebrew (macOS) or Chocolatey (Windows).

  • Terraform: The core IaC tool we’ll be using.
  • Go: The programming language for writing our integration tests.
  • Docker: The container platform needed to run LocalStack.
  • AWS CLI v2: Useful for interacting with and debugging our LocalStack instance.

Running LocalStack with Docker Compose

The easiest way to run LocalStack is with Docker Compose. Create a docker-compose.yml file with the following content. This configuration exposes the necessary ports and sets up a persistent volume for the LocalStack state.

version: "3.8"

services:
  localstack:
    container_name: "localstack_main"
    image: localstack/localstack:latest
    ports:
      - "127.0.0.1:4566:4566"            # LocalStack Gateway
      - "127.0.0.1:4510-4559:4510-4559"  # External services
    environment:
      - DEBUG=${DEBUG-}
      - DOCKER_HOST=unix:///var/run/docker.sock
    volumes:
      - "${LOCALSTACK_VOLUME_DIR:-./volume}:/var/lib/localstack"
      - "/var/run/docker.sock:/var/run/docker.sock"

Start LocalStack by running the following command in the same directory as your file:

docker-compose up -d

You can verify that it’s running correctly by checking the logs or using the AWS CLI, configured for the local endpoint:

aws --endpoint-url=http://localhost:4566 s3 ls

If this command returns an empty list without errors, your local AWS cloud is ready!

Crafting Your Terraform Configuration for LocalStack

The key to using Terraform with LocalStack is to configure the AWS provider to target your local endpoints instead of the official AWS APIs. This is surprisingly simple.

The provider Block: Pointing Terraform to LocalStack

In your Terraform configuration file (e.g., main.tf), you’ll define the aws provider with custom endpoints. This tells Terraform to direct all API calls for the specified services to your local container.

Important: For this to work seamlessly, you must use dummy values for access_key and secret_key. LocalStack doesn’t validate credentials by default.

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

  endpoints {
    s3 = "http://localhost:4566"
    # Add other services here, e.g.,
    # dynamodb = "http://localhost:4566"
    # lambda   = "http://localhost:4566"
  }
}

Example: Defining an S3 Bucket

Now, let’s define a simple resource. We’ll create an S3 bucket with a specific name and a tag. Add this to your main.tf file:

resource "aws_s3_bucket" "test_bucket" {
  bucket = "my-unique-local-test-bucket"

  tags = {
    Environment = "Development"
    ManagedBy   = "Terraform"
  }
}

output "bucket_name" {
  value = aws_s3_bucket.test_bucket.id
}

With this configuration, you can now run terraform init and terraform apply. Terraform will communicate with your LocalStack container and create the S3 bucket locally.

Writing Go Tests with the AWS SDK for your Terraform LocalStack Setup

Now for the exciting part: writing automated tests in Go to validate the infrastructure that Terraform creates. We will use the official AWS SDK for Go V2, configuring it to point to our LocalStack instance.

Initializing the Go Project

In the same directory, initialize a Go module:

go mod init terraform-localstack-test
go get github.com/aws/aws-sdk-go-v2
go get github.com/aws/aws-sdk-go-v2/config
go get github.com/aws/aws-sdk-go-v2/service/s3
go get github.com/aws/aws-sdk-go-v2/aws

Configuring the AWS Go SDK v2 for LocalStack

To make the Go SDK talk to LocalStack, we need to provide a custom configuration. This involves creating a custom endpoint resolver and disabling credential checks. Create a helper file, perhaps aws_config.go, to handle this logic.

// aws_config.go
package main

import (
	"context"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
)

const (
	awsRegion    = "us-east-1"
	localstackEP = "http://localhost:4566"
)

// newAWSConfig creates a new AWS SDK v2 configuration pointed at LocalStack
func newAWSConfig(ctx context.Context) (aws.Config, error) {
	// Custom resolver for LocalStack endpoints
	customResolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
		return aws.Endpoint{
			URL:           localstackEP,
			SigningRegion: region,
			Source:        aws.EndpointSourceCustom,
		}, nil
	})

	// Load default config and override with custom settings
	return config.LoadDefaultConfig(ctx,
		config.WithRegion(awsRegion),
		config.WithEndpointResolverWithOptions(customResolver),
		config.WithCredentialsProvider(aws.AnonymousCredentials{}),
	)
}

Writing the Integration Test: A Practical Example

Now, let’s write the test file main_test.go. We’ll use Go’s standard testing package. The test will create an S3 client using our custom configuration and then perform checks against the S3 bucket created by Terraform.

Test Case 1: Verifying S3 Bucket Creation

This test will check if the bucket exists. The HeadBucket API call is a lightweight way to do this; it succeeds if the bucket exists and you have permission, and fails otherwise.

// main_test.go
package main

import (
	"context"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"testing"
)

func TestS3BucketExists(t *testing.T) {
	// Arrange
	ctx := context.TODO()
	bucketName := "my-unique-local-test-bucket"

	cfg, err := newAWSConfig(ctx)
	if err != nil {
		t.Fatalf("failed to create aws config: %v", err)
	}

	s3Client := s3.NewFromConfig(cfg)

	// Act
	_, err = s3Client.HeadBucket(ctx, &s3.HeadBucketInput{
		Bucket: &bucketName,
	})

	// Assert
	if err != nil {
		t.Errorf("HeadBucket failed for bucket '%s': %v", bucketName, err)
	}
}

Test Case 2: Checking Bucket Tagging

A good test goes beyond mere existence. Let’s verify that the tags we defined in our Terraform code were applied correctly.

// Add this test to main_test.go
func TestS3BucketHasCorrectTags(t *testing.T) {
	// Arrange
	ctx := context.TODO()
	bucketName := "my-unique-local-test-bucket"
	expectedTags := map[string]string{
		"Environment": "Development",
		"ManagedBy":   "Terraform",
	}

	cfg, err := newAWSConfig(ctx)
	if err != nil {
		t.Fatalf("failed to create aws config: %v", err)
	}
	s3Client := s3.NewFromConfig(cfg)

	// Act
	output, err := s3Client.GetBucketTagging(ctx, &s3.GetBucketTaggingInput{
		Bucket: &bucketName,
	})
	if err != nil {
		t.Fatalf("GetBucketTagging failed: %v", err)
	}

	// Assert
	actualTags := make(map[string]string)
	for _, tag := range output.TagSet {
		actualTags[*tag.Key] = *tag.Value
	}

	for key, expectedValue := range expectedTags {
		actualValue, ok := actualTags[key]
		if !ok {
			t.Errorf("Expected tag '%s' not found", key)
			continue
		}
		if actualValue != expectedValue {
			t.Errorf("Tag '%s' has wrong value. Got: '%s', Expected: '%s'", key, actualValue, expectedValue)
		}
	}
}

The Complete Workflow: Tying It All Together

Now you have all the pieces. Here is the end-to-end workflow for developing and testing your infrastructure locally.

Step 1: Start LocalStack

Ensure your local cloud is running.

docker-compose up -d

Step 2: Apply Terraform Configuration

Initialize Terraform (if you haven’t already) and apply your configuration to provision the resources inside the LocalStack container.

terraform init
terraform apply -auto-approve

Step 3: Run the Go Integration Tests

Execute your test suite to validate the infrastructure.

go test -v

If all tests pass, you have a high degree of confidence that your Terraform code correctly defines the infrastructure you intended.

Step 4: Tear Down the Infrastructure

After testing, clean up the resources in LocalStack and, if desired, stop the container.

terraform destroy -auto-approve
docker-compose down

Frequently Asked Questions

1. Is LocalStack free?
LocalStack has a free, open-source Community version that covers many core AWS services like S3, DynamoDB, Lambda, and SQS. More advanced services are available in the Pro/Team versions.

2. How does this compare to Terratest?
Terratest is another excellent framework for testing Terraform code, also written in Go. The approach described here is complementary. You can use Terratest’s helper functions to run terraform apply and then use the AWS SDK configuration method shown in this article to point your Terratest assertions at a LocalStack endpoint.

3. Can I use other languages for testing?
Absolutely! The core principle is configuring the AWS SDK of your chosen language (Python’s Boto3, JavaScript’s AWS-SDK, etc.) to use the LocalStack endpoint. The logic remains the same.

4. What if a service isn’t supported by LocalStack?
While LocalStack’s service coverage is extensive, it’s not 100%. For unsupported services, you may need to rely on mocks, stubs, or targeted tests against a real (sandboxed) AWS environment. Always check the official LocalStack documentation for the latest service coverage.

Test Terraform with LocalStack Go Client

Conclusion

Adopting a local-first testing strategy is a paradigm shift for cloud infrastructure development. By combining the declarative power of Terraform with the high-fidelity emulation of LocalStack, you can build a fast, reliable, and cost-effective testing loop. Writing integration tests in Go with the AWS SDK provides the final piece of the puzzle, allowing you to programmatically verify that your infrastructure behaves exactly as expected. This Terraform LocalStack workflow not only accelerates your development cycle but also dramatically improves the quality and reliability of your infrastructure deployments, giving you and your team the confidence to innovate and deploy with speed. Thank you for reading the DevopsRoles page!

,

About HuuPV

My name is Huu. I love technology, especially Devops Skill such as Docker, vagrant, git, and so forth. I like open-sources, so I created DevopsRoles.com to share the knowledge I have acquired. My Job: IT system administrator. Hobbies: summoners war game, gossip.
View all posts by HuuPV →

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.