Why IaC Matters for QA

Infrastructure as Code (IaC) means defining your infrastructure — servers, databases, networks, load balancers — in configuration files rather than creating them manually through web consoles or CLI commands.

For QA engineers, IaC transforms how test environments are managed. Instead of asking a DevOps engineer to “set up a staging environment” (which might take days and produce inconsistent results), you define the environment in code and create it with a single command.

IaC Principles

Declarative vs. Imperative

Declarative (what you want): “I want a PostgreSQL database, version 15, with 4GB RAM.” The tool figures out how to make it happen. Terraform uses this approach.

Imperative (how to do it): “Install PostgreSQL. Configure memory to 4GB. Create user. Set permissions.” You specify every step. Ansible uses this approach for configuration.

Key Principles

PrincipleDescriptionQA Benefit
Version controlInfrastructure code lives in GitTrack changes, review modifications
IdempotencyRunning the same code twice produces the same resultReliable environment creation
ReproducibilitySame code creates identical environments every timeConsistent test environments
Self-documentingThe code IS the documentationNo outdated wiki pages
ReviewableInfrastructure changes go through code reviewCatch mistakes before they affect environments

Terraform Basics

Terraform is the most popular IaC tool. It works with all major cloud providers (AWS, GCP, Azure) and many other services.

HCL (HashiCorp Configuration Language)

# Define a test database
resource "aws_db_instance" "test_db" {
  identifier     = "qa-test-database"
  engine         = "postgres"
  engine_version = "15.4"
  instance_class = "db.t3.micro"

  db_name  = "testdb"
  username = "test_user"
  password = var.db_password

  allocated_storage = 20

  tags = {
    Environment = "qa"
    ManagedBy   = "terraform"
  }
}

# Define a test application server
resource "aws_instance" "test_app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.small"

  tags = {
    Name        = "qa-test-app"
    Environment = "qa"
  }
}

Terraform Workflow

# Initialize Terraform (download providers)
terraform init

# Preview changes
terraform plan

# Apply changes (create/modify infrastructure)
terraform apply

# Destroy infrastructure
terraform destroy

Variables and Outputs

# variables.tf
variable "environment" {
  description = "Environment name"
  type        = string
  default     = "qa"
}

variable "db_password" {
  description = "Database password"
  type        = string
  sensitive   = true
}

# outputs.tf
output "database_url" {
  value = "postgresql://${aws_db_instance.test_db.username}@${aws_db_instance.test_db.endpoint}/${aws_db_instance.test_db.db_name}"
}

output "app_url" {
  value = "http://${aws_instance.test_app.public_ip}:3000"
}

IaC for Test Environments

Pattern: Reusable Test Environment Module

# modules/test-environment/main.tf
module "test_env" {
  source = "./modules/test-environment"

  environment_name = "pr-${var.pr_number}"
  app_version      = var.git_sha
  db_version       = "15"

  enable_monitoring = false
  instance_size     = "small"
}

This module encapsulates everything needed for a test environment. Creating a new one for each PR becomes a one-line change.

Pattern: Environment Lifecycle in CI

# GitHub Actions
jobs:
  create-env:
    steps:
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform apply -auto-approve -var="pr_number=${{ github.event.pull_request.number }}"

  run-tests:
    needs: create-env
    steps:
      - run: npx playwright test --base-url=$(terraform output -raw app_url)

  destroy-env:
    needs: run-tests
    if: always()
    steps:
      - run: terraform destroy -auto-approve

Exercise: Define Test Infrastructure

Write Terraform configuration for a test environment with:

  • PostgreSQL database (version 15)
  • Redis cache
  • Application server
  • Outputs for connection strings
Solution
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

variable "environment" {
  type    = string
  default = "qa"
}

resource "aws_db_instance" "postgres" {
  identifier     = "${var.environment}-postgres"
  engine         = "postgres"
  engine_version = "15.4"
  instance_class = "db.t3.micro"
  db_name        = "testdb"
  username       = "testuser"
  password       = "testpass123"
  allocated_storage = 20
  skip_final_snapshot = true

  tags = { Environment = var.environment }
}

resource "aws_elasticache_cluster" "redis" {
  cluster_id      = "${var.environment}-redis"
  engine          = "redis"
  node_type       = "cache.t3.micro"
  num_cache_nodes = 1

  tags = { Environment = var.environment }
}

resource "aws_instance" "app" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t3.small"

  user_data = <<-EOF
    #!/bin/bash
    export DATABASE_URL="postgresql://testuser:testpass123@${aws_db_instance.postgres.endpoint}/testdb"
    export REDIS_URL="redis://${aws_elasticache_cluster.redis.cache_nodes[0].address}:6379"
    cd /app && npm start
  EOF

  tags = { Name = "${var.environment}-app", Environment = var.environment }
}

output "database_url" {
  value     = "postgresql://testuser@${aws_db_instance.postgres.endpoint}/testdb"
  sensitive = true
}

output "app_url" {
  value = "http://${aws_instance.app.public_ip}:3000"
}

IaC Tools Comparison

ToolTypeBest ForLearning Curve
TerraformDeclarative provisioningCloud infrastructure (AWS, GCP, Azure)Medium
AnsibleImperative configurationServer setup and application deploymentLow
PulumiDeclarative (real languages)Teams preferring TypeScript/Python over HCLMedium
CloudFormationDeclarative (AWS only)AWS-exclusive environmentsMedium
HelmDeclarative (K8s only)Kubernetes application deploymentMedium

Key Takeaways

  1. IaC makes test environments reproducible — define once, create many times
  2. Version control infrastructure — changes are tracked, reviewed, and reversible
  3. Terraform plan before apply — always preview changes before creating infrastructure
  4. Use modules for reusable patterns — test environments should be one-line deployments
  5. Destroy after testing — ephemeral infrastructure saves costs and prevents state drift