Master infrastructure testing strategies including linting, unit testing, integration testing, compliance checks, and security scanning for Terraform, Ansible, and CloudFormation.
Master infrastructure testing strategies including linting, unit testing, integration testing, compliance checks, and security scanning for Terraform, Ansible, and CloudFormation.
<p>Infrastructure as Code requires a comprehensive testing strategy to ensure reliability, security, and compliance before deploying to production.</p>
<h4>IaC Testing Pyramid:</h4>
1 /\
2 / \
3 / \
4 / E2E \ <- Slowest, Most Expensive
5 /________\
6 / \
7 / Integration \ <- Test real infrastructure
8 /______________\
9 / \
10 / Unit Tests \ <- Fast, isolated
11 /____________________\
12 / \
13 / Static Analysis \ <- Fastest
14 /__________________________\
15
16Level 1: Static Analysis (seconds)
17- Syntax validation
18- Linting
19- Security scanning
20
21Level 2: Unit Testing (seconds)
22- Module testing
23- Logic validation
24- Mock dependencies
25
26Level 3: Integration Testing (minutes)
27- Real infrastructure
28- Provider validation
29- Dependency testing
30
31Level 4: End-to-End Testing (minutes-hours)
32- Complete environment
33- Application testing
34- Performance testing<h4>Testing Tools Overview:</h4>
<table style="width: 100%; border-collapse: collapse; margin: 15px 0;">
<tr style="background: #f5f5f5;">
<th style="padding: 10px; border: 1px solid #ddd;">Tool</th>
<th style="padding: 10px; border: 1px solid #ddd;">IaC Type</th>
<th style="padding: 10px; border: 1px solid #ddd;">Testing Type</th>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>terraform validate</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">Terraform</td>
<td style="padding: 10px; border: 1px solid #ddd;">Syntax</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>tflint</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">Terraform</td>
<td style="padding: 10px; border: 1px solid #ddd;">Linting</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>Terratest</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">Terraform</td>
<td style="padding: 10px; border: 1px solid #ddd;">Integration</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>ansible-lint</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">Ansible</td>
<td style="padding: 10px; border: 1px solid #ddd;">Linting</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>Molecule</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">Ansible</td>
<td style="padding: 10px; border: 1px solid #ddd;">Integration</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>cfn-lint</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">CloudFormation</td>
<td style="padding: 10px; border: 1px solid #ddd;">Linting</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>TaskCat</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">CloudFormation</td>
<td style="padding: 10px; border: 1px solid #ddd;">Integration</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>checkov</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">Multi (TF, CFN, K8s)</td>
<td style="padding: 10px; border: 1px solid #ddd;">Security</td>
</tr>
<tr>
<td style="padding: 10px; border: 1px solid #ddd;"><strong>tfsec</strong></td>
<td style="padding: 10px; border: 1px solid #ddd;">Terraform</td>
<td style="padding: 10px; border: 1px solid #ddd;">Security</td>
</tr>
</table>
<div style="background: #f0f7ff; border-left: 4px solid #0066cc; padding: 15px; margin: 15px 0;">
<strong>💡 Best Practice:</strong> Run static analysis and linting on every commit with pre-commit hooks. Run integration tests on pull requests. Run full E2E tests before production deployments.
</div>
<p>Terraform provides built-in validation and formatting tools, with community tools for deeper linting.</p>
<h4>Built-in Terraform Validation:</h4>
1# Format check (style validation)
2terraform fmt -check -recursive
3
4# Auto-format files
5terraform fmt -recursive
6
7# Syntax validation (requires init)
8terraform init -backend=false
9terraform validate
10
11# Plan validation (dry run)
12terraform plan -detailed-exitcode
13# Exit codes:
14# 0 = No changes
15# 1 = Error
16# 2 = Changes present<h4>TFLint - Advanced Linting:</h4>
1# Install TFLint
2curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
3
4# Or with Homebrew
5brew install tflint
6
7# Basic usage
8tflint
9
10# With specific config
11tflint --config .tflint.hcl
12
13# Enable all rules
14tflint --enable-rule all
15
16# Check specific directory
17tflint --recursive<h4>TFLint Configuration (.tflint.hcl):</h4>
1# .tflint.hcl
2config {
3 module = true
4 force = false
5}
6
7# AWS Plugin
8plugin "aws" {
9 enabled = true
10 version = "0.27.0"
11 source = "github.com/terraform-linters/tflint-ruleset-aws"
12}
13
14# Google Plugin
15plugin "google" {
16 enabled = true
17 version = "0.27.1"
18 source = "github.com/terraform-linters/tflint-ruleset-google"
19}
20
21# Enable specific rules
22rule "terraform_deprecated_interpolation" {
23 enabled = true
24}
25
26rule "terraform_documented_outputs" {
27 enabled = true
28}
29
30rule "terraform_documented_variables" {
31 enabled = true
32}
33
34rule "terraform_naming_convention" {
35 enabled = true
36 format = "snake_case"
37}
38
39rule "terraform_unused_declarations" {
40 enabled = true
41}
42
43# AWS-specific rules
44rule "aws_instance_invalid_type" {
45 enabled = true
46}
47
48rule "aws_instance_previous_type" {
49 enabled = true
50}
51
52rule "aws_db_instance_default_parameter_group" {
53 enabled = true
54}<h4>terraform-docs - Documentation Generation:</h4>
1# Install
2go install github.com/terraform-docs/terraform-docs@latest
3# Or: brew install terraform-docs
4
5# Generate markdown documentation
6terraform-docs markdown table . > README.md
7
8# Update existing README between markers
9terraform-docs markdown table --output-file README.md --output-mode inject .
10
11# Configuration (.terraform-docs.yml)
12formatter: "markdown table"
13
14header-from: "docs/header.md"
15
16sections:
17 show:
18 - header
19 - inputs
20 - outputs
21 - providers
22 - requirements
23
24output:
25 file: "README.md"
26 mode: inject
27 template: |-
28 <!-- BEGIN_TF_DOCS -->
29 {{ .Content }}
30 <!-- END_TF_DOCS --><h4>Pre-commit Configuration:</h4>
1# .pre-commit-config.yaml
2repos:
3 - repo: https://github.com/antonbabenko/pre-commit-terraform
4 rev: v1.86.0
5 hooks:
6 - id: terraform_fmt
7 - id: terraform_validate
8 - id: terraform_docs
9 args:
10 - --args=--config=.terraform-docs.yml
11 - id: terraform_tflint
12 args:
13 - --args=--config=.tflint.hcl
14 - id: terraform_tfsec
15 - id: terraform_checkov
16 args:
17 - --args=--quiet --compact
18
19 - repo: https://github.com/pre-commit/pre-commit-hooks
20 rev: v4.5.0
21 hooks:
22 - id: check-merge-conflict
23 - id: end-of-file-fixer
24 - id: trailing-whitespace
25
26# Install hooks
27pre-commit install
28pre-commit run --all-files<p>Security scanning is essential to catch misconfigurations and vulnerabilities before they reach production.</p>
<h4>Checkov - Multi-Tool Security Scanner:</h4>
1# Install Checkov
2pip install checkov
3
4# Or with Docker
5docker pull bridgecrew/checkov
6
7# Scan Terraform
8checkov -d /path/to/terraform
9
10# Scan specific file
11checkov -f main.tf
12
13# Scan with specific framework
14checkov -d . --framework terraform
15
16# Skip checks
17checkov -d . --skip-check CKV_AWS_19,CKV_AWS_20
18
19# Output formats
20checkov -d . -o json > results.json
21checkov -d . -o junitxml > results.xml
22
23# Compact output
24checkov -d . --compact --quiet
25
26# With baseline (ignore known issues)
27checkov -d . --baseline baseline.json<h4>Common Checkov Rules:</h4>
1# S3 Security
2CKV_AWS_19 - S3 Bucket has server-side encryption
3CKV_AWS_20 - S3 Bucket should block public access
4CKV_AWS_21 - S3 Bucket has versioning enabled
5
6# EC2 Security
7CKV_AWS_79 - EC2 metadata service v2
8CKV_AWS_88 - EC2 instance should have EBS encryption
9
10# RDS Security
11CKV_AWS_16 - RDS instances should have encryption
12CKV_AWS_17 - RDS instances should not be publicly accessible
13CKV_AWS_161 - RDS deletion protection enabled
14
15# IAM Security
16CKV_AWS_40 - IAM policies should not use wildcards
17CKV_AWS_62 - IAM roles should have limited permissions
18
19# Networking
20CKV_AWS_23 - Security groups should not allow unrestricted ingress
21CKV_AWS_24 - Security groups should not allow 0.0.0.0/0 to SSH<h4>TFSec - Terraform Security Scanner:</h4>
1# Install
2brew install tfsec
3# Or: go install github.com/aquasecurity/tfsec/cmd/tfsec@latest
4
5# Basic scan
6tfsec .
7
8# Output formats
9tfsec . --format json > results.json
10tfsec . --format junit > results.xml
11tfsec . --format markdown > results.md
12
13# Exclude checks
14tfsec . --exclude-downloaded-modules
15
16# With config file
17tfsec . --config-file tfsec.yml<h4>TFSec Configuration (tfsec.yml):</h4>
1# tfsec.yml
2minimum_severity: MEDIUM
3
4exclude:
5 - aws-s3-enable-bucket-logging # Handled by centralized logging
6 - aws-ec2-enforce-http-token-imds
7
8severity_overrides:
9 aws-s3-specify-public-access-block: HIGH
10
11custom_checks:
12 - id: CUS001
13 description: "Ensure all S3 buckets have a specific tag"
14 check:
15 type: aws_s3_bucket
16 subpath: tags
17 required: true
18 contains: "data-classification"<h4>Trivy - Vulnerability Scanner:</h4>
1# Install
2brew install trivy
3# Or: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
4
5# Scan IaC files
6trivy config /path/to/terraform
7
8# Scan with severity filter
9trivy config . --severity HIGH,CRITICAL
10
11# Output formats
12trivy config . --format json > results.json
13
14# Skip specific checks
15trivy config . --skip-dirs .terraform<h4>Integrated Security Pipeline:</h4>
1# .github/workflows/security.yml
2name: IaC Security Scan
3
4on: [push, pull_request]
5
6jobs:
7 security:
8 runs-on: ubuntu-latest
9 steps:
10 - uses: actions/checkout@v4
11
12 - name: Run Checkov
13 uses: bridgecrewio/checkov-action@v12
14 with:
15 directory: .
16 framework: terraform
17 output_format: sarif
18 output_file_path: checkov-results.sarif
19
20 - name: Run TFSec
21 uses: aquasecurity/[email protected]
22 with:
23 working_directory: .
24 format: sarif
25 sarif_file: tfsec-results.sarif
26
27 - name: Run Trivy
28 uses: aquasecurity/trivy-action@master
29 with:
30 scan-type: config
31 scan-ref: .
32 format: sarif
33 output: trivy-results.sarif
34
35 - name: Upload SARIF results
36 uses: github/codeql-action/upload-sarif@v2
37 with:
38 sarif_file: .
39 category: iac-security<div style="background: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 15px 0;">
<strong>⚠️ Important:</strong> Security scanning should be mandatory in CI/CD pipelines. Block deployments if critical or high severity issues are found.
</div>
<p>Policy as Code allows you to define and enforce compliance rules programmatically using tools like OPA, Sentinel, or Conftest.</p>
<h4>Open Policy Agent (OPA) with Conftest:</h4>
1# Install Conftest
2brew install conftest
3
4# Or download binary
5wget https://github.com/open-policy-agent/conftest/releases/download/v0.46.2/conftest_0.46.2_Linux_x86_64.tar.gz
6
7# Basic usage
8conftest test main.tf
9
10# Specify policy directory
11conftest test --policy ./policy main.tf
12
13# Multiple files
14conftest test --policy ./policy *.tf<h4>Writing Rego Policies:</h4>
1# policy/terraform.rego
2package terraform
3
4# Deny S3 buckets without encryption
5deny[msg] {
6 resource := input.resource.aws_s3_bucket[name]
7 not resource.server_side_encryption_configuration
8 msg := sprintf("S3 bucket '%s' must have server-side encryption enabled", [name])
9}
10
11# Deny public S3 buckets
12deny[msg] {
13 resource := input.resource.aws_s3_bucket[name]
14 resource.acl == "public-read"
15 msg := sprintf("S3 bucket '%s' must not be public", [name])
16}
17
18# Require specific tags
19required_tags := ["Environment", "Project", "Owner"]
20
21deny[msg] {
22 resource := input.resource.aws_instance[name]
23 tags := resource.tags
24 missing := [tag | tag := required_tags[_]; not tags[tag]]
25 count(missing) > 0
26 msg := sprintf("EC2 instance '%s' missing required tags: %v", [name, missing])
27}
28
29# Deny unrestricted security groups
30deny[msg] {
31 resource := input.resource.aws_security_group[name]
32 ingress := resource.ingress[_]
33 ingress.cidr_blocks[_] == "0.0.0.0/0"
34 ingress.from_port == 22
35 msg := sprintf("Security group '%s' allows SSH from 0.0.0.0/0", [name])
36}
37
38# Enforce instance types
39allowed_instance_types := ["t3.micro", "t3.small", "t3.medium"]
40
41deny[msg] {
42 resource := input.resource.aws_instance[name]
43 not resource.instance_type in allowed_instance_types
44 msg := sprintf("Instance '%s' uses non-approved type '%s'. Allowed: %v", [name, resource.instance_type, allowed_instance_types])
45}
46
47# Require encryption at rest
48deny[msg] {
49 resource := input.resource.aws_rds_instance[name]
50 not resource.storage_encrypted
51 msg := sprintf("RDS instance '%s' must have storage encryption enabled", [name])
52}<h4>HashiCorp Sentinel (Terraform Cloud/Enterprise):</h4>
1# Sentinel Policy Example
2# prevent-destroy.sentinel
3import "tfplan"
4import "strings"
5
6# Find all resources being destroyed
7destroyed = filter tfplan.resource_changes as _, rc {
8 rc.change.actions contains "delete"
9}
10
11# Prevent destruction of production databases
12main = rule {
13 all destroyed as _, resource {
14 not (strings.has_prefix(resource.address, "aws_rds_instance") and
15 resource.change.before.tags["Environment"] == "production")
16 }
17}
18
19# enforce-tagging.sentinel
20import "tfplan"
21
22required_tags = ["Environment", "Project", "CostCenter"]
23
24# Check for required tags on all taggable resources
25validate_tags = func(resources) {
26 violations = []
27 for resources as address, rc {
28 for required_tags as tag {
29 if rc.change.after.tags[tag] is null {
30 append(violations, address + " missing tag: " + tag)
31 }
32 }
33 }
34 return violations
35}
36
37# Get all EC2 instances and S3 buckets
38ec2_instances = filter tfplan.resource_changes as _, rc {
39 rc.type is "aws_instance"
40}
41
42main = rule {
43 length(validate_tags(ec2_instances)) is 0
44}<h4>OPA Gatekeeper for Kubernetes:</h4>
1# Constraint Template
2apiVersion: templates.gatekeeper.sh/v1
3kind: ConstraintTemplate
4metadata:
5 name: k8srequiredlabels
6spec:
7 crd:
8 spec:
9 names:
10 kind: K8sRequiredLabels
11 validation:
12 openAPIV3Schema:
13 properties:
14 labels:
15 type: array
16 items: string
17 targets:
18 - target: admission.k8s.gatekeeper.sh
19 rego: |
20 package k8srequiredlabels
21
22 violation[{"msg": msg}] {
23 provided := {label | input.review.object.metadata.labels[label]}
24 required := {label | label := input.parameters.labels[_]}
25 missing := required - provided
26 count(missing) > 0
27 msg := sprintf("Missing required labels: %v", [missing])
28 }
29
30---
31# Constraint
32apiVersion: constraints.gatekeeper.sh/v1beta1
33kind: K8sRequiredLabels
34metadata:
35 name: require-standard-labels
36spec:
37 match:
38 kinds:
39 - apiGroups: [""]
40 kinds: ["Pod", "Deployment", "Service"]
41 parameters:
42 labels:
43 - "app"
44 - "environment"
45 - "owner"<h4>CI/CD Policy Integration:</h4>
1# .github/workflows/policy-check.yml
2name: Policy Check
3
4on: [push, pull_request]
5
6jobs:
7 policy:
8 runs-on: ubuntu-latest
9 steps:
10 - uses: actions/checkout@v4
11
12 - name: Setup Terraform
13 uses: hashicorp/setup-terraform@v3
14
15 - name: Terraform Init
16 run: terraform init -backend=false
17
18 - name: Convert to JSON
19 run: terraform show -json tfplan > tfplan.json
20
21 - name: Setup Conftest
22 uses: instrumenta/[email protected]
23
24 - name: Run Policy Check
25 run: |
26 conftest test tfplan.json --policy ./policy \
27 --output table --all-namespaces<div style="background: #e8f5e9; border-left: 4px solid #4caf50; padding: 15px; margin: 15px 0;">
<strong>✅ Policy Categories:</strong>
<ul>
<li><strong>Security:</strong> Encryption, network access, IAM</li>
<li><strong>Compliance:</strong> Tagging, naming, resource types</li>
<li><strong>Cost:</strong> Instance sizes, resource limits</li>
<li><strong>Operational:</strong> Backups, monitoring, logging</li>
</ul>
</div>
<p>Terratest is a Go library that makes it easier to write automated tests for infrastructure code.</p>
<h4>Installation:</h4>
1# Initialize Go module
2go mod init github.com/yourorg/infrastructure-tests
3
4# Add Terratest
5go get github.com/gruntwork-io/terratest/modules/terraform
6
7# Install AWS SDK (if testing AWS)
8go get github.com/aws/aws-sdk-go/aws<h4>Basic Terraform Test:</h4>
1// test/terraform_vpc_test.go
2package test
3
4import (
5 "testing"
6
7 "github.com/gruntwork-io/terratest/modules/terraform"
8 "github.com/stretchr/testify/assert"
9)
10
11func TestVPCCreation(t *testing.T) {
12 t.Parallel()
13
14 terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
15 // Path to Terraform code
16 TerraformDir: "../modules/vpc",
17
18 // Variables
19 Vars: map[string]interface{}{
20 "vpc_cidr": "10.0.0.0/16",
21 "environment": "test",
22 "public_subnets": []string{"10.0.1.0/24", "10.0.2.0/24"},
23 "private_subnets": []string{"10.0.10.0/24", "10.0.20.0/24"},
24 },
25
26 // Environment variables
27 EnvVars: map[string]string{
28 "AWS_DEFAULT_REGION": "us-west-2",
29 },
30 })
31
32 // Clean up resources at end of test
33 defer terraform.Destroy(t, terraformOptions)
34
35 // Create resources
36 terraform.InitAndApply(t, terraformOptions)
37
38 // Get outputs
39 vpcId := terraform.Output(t, terraformOptions, "vpc_id")
40 publicSubnetIds := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
41 privateSubnetIds := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
42
43 // Assertions
44 assert.NotEmpty(t, vpcId)
45 assert.Len(t, publicSubnetIds, 2)
46 assert.Len(t, privateSubnetIds, 2)
47}<h4>Testing AWS Resources:</h4>
1// test/terraform_ec2_test.go
2package test
3
4import (
5 "testing"
6 "time"
7
8 "github.com/gruntwork-io/terratest/modules/aws"
9 "github.com/gruntwork-io/terratest/modules/http-helper"
10 "github.com/gruntwork-io/terratest/modules/retry"
11 "github.com/gruntwork-io/terratest/modules/terraform"
12 "github.com/stretchr/testify/assert"
13)
14
15func TestEC2Instance(t *testing.T) {
16 t.Parallel()
17
18 region := "us-west-2"
19
20 terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
21 TerraformDir: "../modules/ec2",
22 Vars: map[string]interface{}{
23 "instance_type": "t3.micro",
24 "environment": "test",
25 },
26 EnvVars: map[string]string{
27 "AWS_DEFAULT_REGION": region,
28 },
29 })
30
31 defer terraform.Destroy(t, terraformOptions)
32 terraform.InitAndApply(t, terraformOptions)
33
34 instanceId := terraform.Output(t, terraformOptions, "instance_id")
35 publicIp := terraform.Output(t, terraformOptions, "public_ip")
36
37 // Verify instance exists and is running
38 instanceState := aws.GetInstanceState(t, region, instanceId)
39 assert.Equal(t, "running", instanceState)
40
41 // Test HTTP endpoint
42 url := "http://" + publicIp
43 expectedStatus := 200
44 expectedBody := "Hello, World"
45
46 retry.DoWithRetry(t, "HTTP GET", 10, 10*time.Second, func() (string, error) {
47 return http_helper.HttpGetWithValidation(t, url, nil, expectedStatus, expectedBody)
48 })
49}<h4>Testing RDS Database:</h4>
1// test/terraform_rds_test.go
2package test
3
4import (
5 "database/sql"
6 "fmt"
7 "testing"
8
9 "github.com/gruntwork-io/terratest/modules/terraform"
10 _ "github.com/lib/pq"
11 "github.com/stretchr/testify/assert"
12)
13
14func TestRDSDatabase(t *testing.T) {
15 t.Parallel()
16
17 terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
18 TerraformDir: "../modules/rds",
19 Vars: map[string]interface{}{
20 "db_name": "testdb",
21 "db_username": "testuser",
22 "db_password": "testpassword123",
23 "instance_class": "db.t3.micro",
24 "allocated_storage": 20,
25 },
26 })
27
28 defer terraform.Destroy(t, terraformOptions)
29 terraform.InitAndApply(t, terraformOptions)
30
31 endpoint := terraform.Output(t, terraformOptions, "db_endpoint")
32 port := terraform.Output(t, terraformOptions, "db_port")
33
34 // Test database connection
35 connStr := fmt.Sprintf(
36 "host=%s port=%s user=testuser password=testpassword123 dbname=testdb sslmode=require",
37 endpoint, port,
38 )
39
40 db, err := sql.Open("postgres", connStr)
41 assert.NoError(t, err)
42 defer db.Close()
43
44 err = db.Ping()
45 assert.NoError(t, err)
46
47 // Test query
48 var result int
49 err = db.QueryRow("SELECT 1").Scan(&result)
50 assert.NoError(t, err)
51 assert.Equal(t, 1, result)
52}<h4>Running Tests:</h4>
1# Run all tests
2go test -v ./test/...
3
4# Run specific test
5go test -v -run TestVPCCreation ./test/...
6
7# Run with timeout
8go test -v -timeout 30m ./test/...
9
10# Run in parallel with limited parallelism
11go test -v -parallel 2 ./test/...
12
13# Run with coverage
14go test -v -cover ./test/...
15
16# Generate test report
17go test -v -json ./test/... > test-results.json<h4>Test Fixtures and Stages:</h4>
1// test/fixtures/vpc/main.tf
2terraform {
3 backend "local" {
4 path = "terraform.tfstate"
5 }
6}
7
8module "vpc" {
9 source = "../../../modules/vpc"
10
11 vpc_cidr = var.vpc_cidr
12 environment = var.environment
13 public_subnets = var.public_subnets
14 private_subnets = var.private_subnets
15}
16
17// test/terraform_stages_test.go
18func TestInfrastructureStaged(t *testing.T) {
19 t.Parallel()
20
21 // Stage 1: Deploy VPC
22 vpcOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
23 TerraformDir: "../fixtures/vpc",
24 })
25
26 defer terraform.Destroy(t, vpcOptions)
27 terraform.InitAndApply(t, vpcOptions)
28
29 vpcId := terraform.Output(t, vpcOptions, "vpc_id")
30
31 // Stage 2: Deploy Application (depends on VPC)
32 appOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
33 TerraformDir: "../fixtures/app",
34 Vars: map[string]interface{}{
35 "vpc_id": vpcId,
36 },
37 })
38
39 defer terraform.Destroy(t, appOptions)
40 terraform.InitAndApply(t, appOptions)
41}<p>Ansible testing involves linting, syntax checking, and integration testing with Molecule.</p>
<h4>Ansible-Lint:</h4>
1# Install
2pip install ansible-lint
3
4# Lint playbook
5ansible-lint playbook.yml
6
7# Lint role
8ansible-lint roles/webserver/
9
10# Lint with specific rules
11ansible-lint -r ~/.ansible/lint-rules playbook.yml
12
13# Skip rules
14ansible-lint -x yaml[trailing-spaces] playbook.yml
15
16# Output format
17ansible-lint --format codeclimate playbook.yml > lint-results.json<h4>Ansible-Lint Configuration (.ansible-lint):</h4>
1# .ansible-lint
2---
3profile: production
4
5# Enable specific rules
6enable_list:
7 - args
8 - empty-string-compare
9 - no-changed-when
10
11# Skip specific rules
12skip_list:
13 - yaml[line-length]
14 - name[casing]
15
16# Exclude paths
17exclude_paths:
18 - .github/
19 - molecule/
20
21# Treat warnings as errors
22strict: true
23
24# Offline mode
25offline: false
26
27# Custom rules location
28rulesdir:
29 - ~/.ansible/lint-rules<h4>Molecule - Comprehensive Role Testing:</h4>
1# Install Molecule
2pip install molecule molecule-docker ansible-lint
3
4# Initialize Molecule in existing role
5cd roles/webserver
6molecule init scenario -d docker
7
8# Create new role with Molecule
9molecule init role company.webserver
10
11# Directory structure
12roles/webserver/
13└── molecule/
14 └── default/
15 ├── molecule.yml # Configuration
16 ├── converge.yml # Playbook to test
17 ├── verify.yml # Verification tests
18 ├── prepare.yml # Pre-test setup
19 ├── cleanup.yml # Post-test cleanup
20 └── Dockerfile.j2 # Custom container<h4>Molecule Configuration:</h4>
1# molecule/default/molecule.yml
2---
3dependency:
4 name: galaxy
5 options:
6 requirements-file: requirements.yml
7
8driver:
9 name: docker
10
11platforms:
12 - name: ubuntu-22
13 image: geerlingguy/docker-ubuntu2204-ansible:latest
14 pre_build_image: true
15 privileged: true
16 command: /lib/systemd/systemd
17 volumes:
18 - /sys/fs/cgroup:/sys/fs/cgroup:ro
19
20 - name: debian-11
21 image: geerlingguy/docker-debian11-ansible:latest
22 pre_build_image: true
23 privileged: true
24 command: /lib/systemd/systemd
25 volumes:
26 - /sys/fs/cgroup:/sys/fs/cgroup:ro
27
28provisioner:
29 name: ansible
30 config_options:
31 defaults:
32 callback_whitelist: profile_tasks
33 inventory:
34 group_vars:
35 all:
36 ansible_user: root
37 webserver_http_port: 8080
38
39verifier:
40 name: ansible
41
42scenario:
43 test_sequence:
44 - dependency
45 - lint
46 - cleanup
47 - destroy
48 - syntax
49 - create
50 - prepare
51 - converge
52 - idempotence
53 - verify
54 - cleanup
55 - destroy<h4>Molecule Verify Playbook:</h4>
1# molecule/default/verify.yml
2---
3- name: Verify
4 hosts: all
5 gather_facts: true
6
7 tasks:
8 - name: Check nginx package
9 package:
10 name: nginx
11 state: present
12 check_mode: true
13 register: pkg
14
15 - name: Assert nginx is installed
16 assert:
17 that:
18 - not pkg.changed
19
20 - name: Check nginx service
21 service:
22 name: nginx
23 state: started
24 check_mode: true
25 register: svc
26
27 - name: Assert nginx is running
28 assert:
29 that:
30 - not svc.changed
31
32 - name: Check HTTP response
33 uri:
34 url: http://localhost:{{ webserver_http_port }}/
35 status_code: 200
36 register: http_result
37
38 - name: Assert HTTP response is OK
39 assert:
40 that:
41 - http_result.status == 200
42
43 - name: Check configuration file exists
44 stat:
45 path: /etc/nginx/nginx.conf
46 register: config_file
47
48 - name: Assert config file exists
49 assert:
50 that:
51 - config_file.stat.exists
52 - config_file.stat.isreg<h4>Molecule Commands:</h4>
1# Full test cycle
2molecule test
3
4# Individual steps
5molecule create # Create instances
6molecule converge # Apply role
7molecule idempotence # Test idempotence
8molecule verify # Run verification
9molecule destroy # Clean up
10
11# Development workflow
12molecule converge # Apply changes
13molecule verify # Test
14# Make changes...
15molecule converge # Reapply
16molecule verify # Retest
17molecule destroy # Clean up
18
19# Login for debugging
20molecule login -h ubuntu-22
21
22# List instances
23molecule list<h4>Testinfra for Advanced Verification:</h4>
1# Install
2pip install pytest-testinfra
3
4# molecule/default/tests/test_webserver.py
5import pytest
6
7def test_nginx_is_installed(host):
8 nginx = host.package("nginx")
9 assert nginx.is_installed
10
11def test_nginx_is_running(host):
12 nginx = host.service("nginx")
13 assert nginx.is_running
14 assert nginx.is_enabled
15
16def test_nginx_listening(host):
17 socket = host.socket("tcp://0.0.0.0:80")
18 assert socket.is_listening
19
20def test_config_file(host):
21 config = host.file("/etc/nginx/nginx.conf")
22 assert config.exists
23 assert config.is_file
24 assert config.user == "root"
25 assert config.mode == 0o644
26
27def test_http_response(host):
28 output = host.check_output("curl -s http://localhost/")
29 assert "Welcome" in output
30
31# Update molecule.yml verifier
32verifier:
33 name: testinfra
34 options:
35 v: true<p>Integrate all testing stages into CI/CD pipelines for automated validation.</p>
<h4>GitHub Actions Complete Pipeline:</h4>
1# .github/workflows/iac-test.yml
2name: IaC Testing Pipeline
3
4on:
5 push:
6 branches: [main, develop]
7 pull_request:
8 branches: [main]
9
10env:
11 TF_VERSION: "1.6.0"
12 AWS_REGION: "us-west-2"
13
14jobs:
15 # Stage 1: Static Analysis
16 static-analysis:
17 runs-on: ubuntu-latest
18 steps:
19 - uses: actions/checkout@v4
20
21 - name: Setup Terraform
22 uses: hashicorp/setup-terraform@v3
23 with:
24 terraform_version: ${{ env.TF_VERSION }}
25
26 - name: Terraform Format Check
27 run: terraform fmt -check -recursive
28
29 - name: Terraform Init
30 run: terraform init -backend=false
31
32 - name: Terraform Validate
33 run: terraform validate
34
35 - name: TFLint
36 uses: terraform-linters/setup-tflint@v4
37
38 - name: Run TFLint
39 run: |
40 tflint --init
41 tflint --recursive
42
43 # Stage 2: Security Scanning
44 security-scan:
45 runs-on: ubuntu-latest
46 needs: static-analysis
47 steps:
48 - uses: actions/checkout@v4
49
50 - name: Run Checkov
51 uses: bridgecrewio/checkov-action@v12
52 with:
53 directory: .
54 framework: terraform
55 soft_fail: false
56 output_format: sarif
57
58 - name: Run TFSec
59 uses: aquasecurity/[email protected]
60 with:
61 working_directory: .
62 soft_fail: false
63
64 - name: Run Trivy
65 uses: aquasecurity/trivy-action@master
66 with:
67 scan-type: config
68 scan-ref: .
69 severity: HIGH,CRITICAL
70 exit-code: 1
71
72 # Stage 3: Policy Check
73 policy-check:
74 runs-on: ubuntu-latest
75 needs: static-analysis
76 steps:
77 - uses: actions/checkout@v4
78
79 - name: Setup Conftest
80 uses: instrumenta/[email protected]
81
82 - name: Run Policy Check
83 run: |
84 conftest test . --policy ./policy \
85 --output table --all-namespaces
86
87 # Stage 4: Unit Tests
88 unit-tests:
89 runs-on: ubuntu-latest
90 needs: [security-scan, policy-check]
91 steps:
92 - uses: actions/checkout@v4
93
94 - name: Setup Go
95 uses: actions/setup-go@v5
96 with:
97 go-version: '1.21'
98
99 - name: Run Unit Tests
100 run: |
101 cd test
102 go test -v -short ./...
103
104 # Stage 5: Integration Tests (PR only)
105 integration-tests:
106 runs-on: ubuntu-latest
107 needs: unit-tests
108 if: github.event_name == 'pull_request'
109 steps:
110 - uses: actions/checkout@v4
111
112 - name: Configure AWS credentials
113 uses: aws-actions/configure-aws-credentials@v4
114 with:
115 aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
116 aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
117 aws-region: ${{ env.AWS_REGION }}
118
119 - name: Setup Go
120 uses: actions/setup-go@v5
121 with:
122 go-version: '1.21'
123
124 - name: Setup Terraform
125 uses: hashicorp/setup-terraform@v3
126 with:
127 terraform_version: ${{ env.TF_VERSION }}
128 terraform_wrapper: false
129
130 - name: Run Integration Tests
131 run: |
132 cd test
133 go test -v -timeout 30m ./...
134 env:
135 TF_VAR_environment: "test-${{ github.run_id }}"
136
137 # Stage 6: Documentation
138 documentation:
139 runs-on: ubuntu-latest
140 needs: static-analysis
141 steps:
142 - uses: actions/checkout@v4
143
144 - name: Generate Terraform Docs
145 uses: terraform-docs/gh-actions@v1
146 with:
147 working-dir: .
148 output-file: README.md
149 output-method: inject
150 git-push: "true"<h4>GitLab CI Pipeline:</h4>
1# .gitlab-ci.yml
2stages:
3 - validate
4 - security
5 - test
6 - deploy
7
8variables:
9 TF_VERSION: "1.6.0"
10
11validate:
12 stage: validate
13 image: hashicorp/terraform:${TF_VERSION}
14 script:
15 - terraform fmt -check -recursive
16 - terraform init -backend=false
17 - terraform validate
18 - |
19 curl -s https://raw.githubusercontent.com/terraform-linters/tflint/master/install_linux.sh | bash
20 tflint --init && tflint
21
22security:
23 stage: security
24 image: python:3.11
25 script:
26 - pip install checkov
27 - checkov -d . --framework terraform --compact --quiet
28 allow_failure: false
29
30unit-test:
31 stage: test
32 image: golang:1.21
33 script:
34 - cd test
35 - go test -v -short ./...
36
37integration-test:
38 stage: test
39 image: golang:1.21
40 rules:
41 - if: $CI_MERGE_REQUEST_ID
42 script:
43 - cd test
44 - go test -v -timeout 30m ./...
45 environment:
46 name: test
47 on_stop: cleanup
48
49deploy-staging:
50 stage: deploy
51 image: hashicorp/terraform:${TF_VERSION}
52 rules:
53 - if: $CI_COMMIT_BRANCH == "develop"
54 script:
55 - terraform init
56 - terraform apply -auto-approve
57 environment:
58 name: staging<h4>Testing Summary Workflow:</h4>
1┌─────────────────────────────────────────────────────────┐
2│ On Every Commit │
3├─────────────────────────────────────────────────────────┤
4│ 1. Format Check (terraform fmt) │
5│ 2. Syntax Validation (terraform validate) │
6│ 3. Linting (tflint, ansible-lint) │
7│ 4. Security Scan (checkov, tfsec) │
8│ 5. Policy Check (conftest/OPA) │
9│ 6. Unit Tests (Terratest -short) │
10└─────────────────────────────────────────────────────────┘
11 │
12 ▼
13┌─────────────────────────────────────────────────────────┐
14│ On Pull Requests │
15├─────────────────────────────────────────────────────────┤
16│ + Integration Tests (Terratest full) │
17│ + Molecule Tests (Ansible roles) │
18│ + Cost Estimation (Infracost) │
19│ + Plan Review (terraform plan) │
20└─────────────────────────────────────────────────────────┘
21 │
22 ▼
23┌─────────────────────────────────────────────────────────┐
24│ On Merge to Main │
25├─────────────────────────────────────────────────────────┤
26│ + Full E2E Tests │
27│ + Deploy to Staging │
28│ + Smoke Tests │
29│ + Manual Approval (Production) │
30│ + Deploy to Production │
31└─────────────────────────────────────────────────────────┘<div style="background: #e8f5e9; border-left: 4px solid #4caf50; padding: 15px; margin: 15px 0;">
<strong>✅ Testing Checklist:</strong>
<ul>
<li>✅ Pre-commit hooks for fast feedback</li>
<li>✅ Security scanning on every commit</li>
<li>✅ Policy compliance checks</li>
<li>✅ Integration tests on pull requests</li>
<li>✅ Cost estimation before deployment</li>
<li>✅ E2E tests before production</li>
<li>✅ Automated documentation generation</li>
</ul>
</div>
Complete IaC Testing Suite
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>IaC Testing & Validation</title>
7 <style>
8 * {
9 margin: 0;
10 padding: 0;
11 box-sizing: border-box;
12 }
13
14 body {
15 font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17 padding: 20px;
18 line-height: 1.6;
19 }
20
21 .container {
22 max-width: 1400px;
23 margin: 0 auto;
24 }
25
26 .header {
27 background: white;
28 padding: 30px;
29 border-radius: 15px;
30 box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
31 margin-bottom: 30px;
32 text-align: center;
33 }
34
35 .header h1 {
36 color: #667eea;
37 margin-bottom: 10px;
38 font-size: 2.5em;
39 }
40
41 .content-grid {
42 display: grid;
43 grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
44 gap: 25px;
45 margin-bottom: 30px;
46 }
47
48 .card {
49 background: white;
50 padding: 25px;
51 border-radius: 15px;
52 box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
53 }
54
55 .card h3 {
56 color: #667eea;
57 margin-bottom: 15px;
58 padding-bottom: 10px;
59 border-bottom: 3px solid #667eea;
60 }
61
62 .feature-list {
63 list-style: none;
64 }
65
66 .feature-list li {
67 padding: 10px;
68 margin: 8px 0;
69 background: #f8f9ff;
70 border-radius: 8px;
71 border-left: 4px solid #667eea;
72 }
73
74 .terminal {
75 background: #1e1e1e;
76 color: #00ff00;
77 padding: 20px;
78 border-radius: 10px;
79 font-family: 'Courier New', monospace;
80 overflow-x: auto;
81 margin: 15px 0;
82 }
83
84 .terminal .command {
85 color: #00ff00;
86 }
87
88 .terminal .success {
89 color: #4caf50;
90 }
91
92 .terminal .error {
93 color: #f44336;
94 }
95
96 .pyramid {
97 text-align: center;
98 padding: 20px;
99 font-family: monospace;
100 background: #1e1e1e;
101 color: #00ff00;
102 border-radius: 10px;
103 }
104
105 @media (max-width: 768px) {
106 .content-grid {
107 grid-template-columns: 1fr;
108 }
109 }
110 </style>
111</head>
112<body>
113 <div class="container">
114 <div class="header">
115 <h1>🧪 IaC Testing & Validation</h1>
116 <p>Comprehensive testing strategies for infrastructure code</p>
117 </div>
118
119 <div class="content-grid">
120 <div class="card">
121 <h3>📊 Testing Pyramid</h3>
122 <div class="pyramid">
123<pre> /\
124 /E2E\ ← Minutes-Hours
125 /______\
126 / \
127 /Integration\ ← Minutes
128 /______________\
129 / \
130 / Unit Tests \ ← Seconds
131/____________________\
132/ \
133/ Static Analysis \ ← Milliseconds
134/__________________________\</pre>
135 </div>
136 </div>
137
138 <div class="card">
139 <h3>🔧 Tools by Category</h3>
140 <ul class="feature-list">
141 <li><strong>Terraform:</strong> tflint, Terratest, tfsec</li>
142 <li><strong>Ansible:</strong> ansible-lint, Molecule, Testinfra</li>
143 <li><strong>CloudFormation:</strong> cfn-lint, TaskCat</li>
144 <li><strong>Security:</strong> Checkov, Trivy, Snyk</li>
145 <li><strong>Policy:</strong> OPA/Conftest, Sentinel</li>
146 </ul>
147 </div>
148
149 <div class="card">
150 <h3>🛡️ Security Checks</h3>
151 <ul class="feature-list">
152 <li><strong>Encryption:</strong> S3, RDS, EBS at rest</li>
153 <li><strong>Network:</strong> Security groups, NACLs</li>
154 <li><strong>IAM:</strong> Least privilege, no wildcards</li>
155 <li><strong>Logging:</strong> CloudTrail, VPC flow logs</li>
156 <li><strong>Compliance:</strong> CIS benchmarks, SOC2</li>
157 </ul>
158 </div>
159
160 <div class="card">
161 <h3>✅ CI/CD Stages</h3>
162 <ul class="feature-list">
163 <li><strong>On Commit:</strong> Lint, validate, security</li>
164 <li><strong>On PR:</strong> Integration tests, plan</li>
165 <li><strong>On Merge:</strong> E2E tests, staging deploy</li>
166 <li><strong>Manual:</strong> Production approval</li>
167 </ul>
168 </div>
169 </div>
170
171 <div class="card">
172 <h3>💻 Testing Workflow</h3>
173 <div class="terminal">
174<span class="command"># 1. Static Analysis</span>
175$ terraform fmt -check && terraform validate && tflint
176<span class="success">✓ All checks passed</span>
177
178<span class="command"># 2. Security Scan</span>
179$ checkov -d . --framework terraform --compact
180<span class="success">Passed: 42/45 checks</span>
181<span class="error">Failed: 3 checks (CKV_AWS_19, CKV_AWS_20, CKV_AWS_21)</span>
182
183<span class="command"># 3. Policy Check</span>
184$ conftest test . --policy ./policy
185<span class="success">✓ 12 policies passed</span>
186
187<span class="command"># 4. Unit Tests</span>
188$ go test -v -short ./test/...
189<span class="success">--- PASS: TestVPCModule (2.34s)
190--- PASS: TestSecurityGroupRules (1.12s)
191PASS</span>
192
193<span class="command"># 5. Integration Tests</span>
194$ go test -v -timeout 30m ./test/...
195<span class="success">--- PASS: TestFullInfrastructure (423.56s)
196PASS</span>
197 </div>
198 </div>
199 </div>
200</body>
201</html>Your progress is saved in this browser, on this device only.