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
| Principle | Description | QA Benefit |
|---|---|---|
| Version control | Infrastructure code lives in Git | Track changes, review modifications |
| Idempotency | Running the same code twice produces the same result | Reliable environment creation |
| Reproducibility | Same code creates identical environments every time | Consistent test environments |
| Self-documenting | The code IS the documentation | No outdated wiki pages |
| Reviewable | Infrastructure changes go through code review | Catch 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
| Tool | Type | Best For | Learning Curve |
|---|---|---|---|
| Terraform | Declarative provisioning | Cloud infrastructure (AWS, GCP, Azure) | Medium |
| Ansible | Imperative configuration | Server setup and application deployment | Low |
| Pulumi | Declarative (real languages) | Teams preferring TypeScript/Python over HCL | Medium |
| CloudFormation | Declarative (AWS only) | AWS-exclusive environments | Medium |
| Helm | Declarative (K8s only) | Kubernetes application deployment | Medium |
Key Takeaways
- IaC makes test environments reproducible — define once, create many times
- Version control infrastructure — changes are tracked, reviewed, and reversible
- Terraform plan before apply — always preview changes before creating infrastructure
- Use modules for reusable patterns — test environments should be one-line deployments
- Destroy after testing — ephemeral infrastructure saves costs and prevents state drift