Infrastructure as Code testing with Terraform, Test Kitchen, and InSpec

Testing Infrastructure as Code: reliable deployments with Terraform and Kitchen-Terraform

9 min read

Faulty IaC still causes outages and cost spikes. This article lays out a layered test strategy, a Kitchen-Terraform plus InSpec walkthrough for an AWS S3 module, and practices that keep infra tests honest in CI.

Why IaC breaks when testing stays informal

Infrastructure as Code is now central to DevOps, but faulty configurations can still cause outages, security gaps, or surprise bills. Classic test habits often treat infra as secondary to application code, so teams miss environment-specific behavior, module coupling, and drift between declared and deployed state. Without disciplined checks, you get an infrastructure-scale version of “works on my machine”: changes that looked fine locally fail in production because of provider versions, API differences, or hidden dependencies.

A layered strategy: unit, integration, end-to-end, drift

Treat IaC like any critical codebase. Unit-style checks validate modules and syntax. Integration runs prove modules compose in an isolated account or project. End-to-end flows stand up full stacks in disposable environments. Drift detection continuously compares live state to the code definition. Kitchen-Terraform plugs Terraform into Test Kitchen so you can drive real applies and verify outcomes with InSpec against real or mocked providers, giving you a repeatable loop instead of one-off manual clicks.

Example layout for a Terraform module under test

The tree below mirrors a common split: module code at the repository root, Ruby Gemfile for Test Kitchen, a top-level kitchen.yml if you use it, and integration tests under test/integration/default with controls and a suite-scoped kitchen.yml.

Repository layout
terraform-module-s3-bucket/
├── main.tf
├── variables.tf
├── outputs.tf
├── test/
│   ├── integration/
│   │   └── default/
│   │       ├── controls/
│   │       │   └── s3_bucket.rb
│   │       └── kitchen.yml
│   └── unit/
│       └── test_s3_bucket.rb
├── Gemfile
└── kitchen.yml

Terraform module: S3 bucket with versioning and SSE

This minimal module matches current AWS provider behavior (v4 onward): `aws_s3_bucket` only sets the name and tags, while `aws_s3_bucket_versioning` and `aws_s3_bucket_server_side_encryption_configuration` own those settings—nested `versioning` and `server_side_encryption_configuration` blocks on the bucket are read-only and will error if you try to manage them there. Variables stay injectable from Kitchen for unique, disposable resources per run.

main.tf
variable "bucket_name" {
  description = "Name of the S3 bucket"
  type        = string
}

variable "tags" {
  description = "Tags to apply to the bucket"
  type        = map(string)
  default     = {}
}

resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
  tags   = var.tags
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "this" {
  bucket = aws_s3_bucket.this.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

Kitchen-Terraform driver and verifier

The suite pins the Terraform driver and provisioner to the module root, passes a timestamped bucket name plus tags, and points the InSpec verifier at the integration test directory. Ubuntu is a typical platform label even though verification is cloud-side.

test/integration/default/kitchen.yml
driver:
  name: terraform

provisioner:
  name: terraform
  root_folder: ../..
  variables:
    bucket_name: "test-kitchen-s3-bucket-<%= Time.now.to_i %>"
    tags:
      Environment: test
      Kitchen: verifies

verifier:
  name: inspec

platforms:
  - name: ubuntu-22.04

suites:
  - name: default
    verifier:
      inspec_tests:
        - test/integration/default

InSpec control against the live bucket

After apply, InSpec uses the AWS API to assert the bucket exists, versioning is on, and an encryption configuration is present. Failures block promotion because they surface real misconfiguration rather than mocked assumptions.

test/integration/default/controls/s3_bucket.rb
control 's3-bucket-basics' do
  impact 1.0
  title 'Ensure S3 bucket has versioning and encryption'

  describe aws_s3_bucket(bucket_name: input('bucket_name')) do
    it { should exist }
    its('versioning') { should eq true }
    its('server_side_encryption_configuration') { should_not be_empty }
  end
end

Create, verify, destroy

The workflow provisions a real bucket in AWS using short-lived credentials, converges Terraform, runs InSpec, then tears resources down. That is slower than mocks but catches provider-specific behavior you only see against the live API.

Kitchen CLI
# Install dependencies
bundle install

# Create and converge the test instance
bundle exec kitchen create

# Run the verification
bundle exec kitchen verify

# Clean up
bundle exec kitchen destroy

Best practices that keep IaC tests trustworthy

Isolate environments with workspaces or random suffixes so parallel runs never collide. Prefer real providers (or high-fidelity tools like LocalStack) for integration and end-to-end tiers while keeping fast static checks upstream. Add policy-as-code gates with OPA and Conftest or Sentinel before apply. Wire the suite into pull requests with caching tuned for speed versus depth. Version modules semantically and publish to a private registry so consumers pin known-good releases. Track flaky runs, add bounded retries for transient cloud errors, and separate noise from real regressions. Watch costs for ephemeral stacks. Finally, document each control’s intent so the suite stays approachable as the team grows.

To wire these checks into delivery without guesswork, align the suite with the diagnostics in our release pipeline bottlenecks guide.

When infra tests surface intermittent failures, treat observability and controlled failure practice as companions, as in this Chaos Engineering playbook for DevOps.