CloudFormation Template Testing: cfn-lint, TaskCat, and Integration Testing

CloudFormation Template Testing: cfn-lint, TaskCat, and Integration Testing

CloudFormation templates are JSON or YAML files describing AWS infrastructure. Unlike application code, you can't run unit tests against infrastructure templates the same way — the only way to know for certain that a template deploys correctly is to deploy it. But several tools give you meaningful validation before deployment, and TaskCat provides actual multi-region integration testing.

This guide covers the CloudFormation testing toolkit: static analysis with cfn-lint, policy compliance with cfn-guard, and automated integration testing with TaskCat.

Static Analysis with cfn-lint

cfn-lint (CloudFormation Linter) validates CloudFormation templates against the CloudFormation resource specification. It catches:

  • Invalid resource types and property names
  • Incorrect property types (string where integer is expected)
  • Missing required properties
  • Circular dependencies
  • Invalid IAM policy structure
  • References to non-existent resources (within the template)

Installation and basic usage:

pip install cfn-lint

# Lint a single template
cfn-lint template.yaml

# Lint with specific rules
cfn-lint template.yaml --include-checks W  # Include warnings

# Lint all templates in a directory
cfn-lint templates/**/*.yaml

# Output in different formats
cfn-lint template.yaml --format json
cfn-lint template.yaml --format junit

Common findings:

E0000: Template format error (YAML/JSON parsing failure)
E1001: Property type mismatch
E2001: Required property missing
E3001: Invalid resource type
E3012: Required property condition
W1011: FindInMap does not exist
W2001: Unused parameter
W3010: Redundant parameter default

Integrating into CI:

# GitHub Actions
- name: Lint CloudFormation templates
  run: |
    pip install cfn-lint
    cfn-lint templates/**/*.yaml --format junit > cfn-lint-results.xml || true
- name: Publish lint results
  uses: mikepenz/action-junit-report@v3
  with:
    report_paths: cfn-lint-results.xml
# GitLab CI
cfn-lint:
  stage: test
  image: python:3.11
  script:
    - pip install cfn-lint
    - cfn-lint templates/**/*.yaml --format junit > cfn-lint-results.xml
  artifacts:
    when: always
    reports:
      junit: cfn-lint-results.xml

Configuration file (.cfnlintrc.yaml):

# .cfnlintrc.yaml
templates:
  - templates/**/*.yaml
ignore_checks:
  - W1011  # Suppress specific warnings
  - W2001
include_checks:
  - W     # Include all warnings
configure_rules:
  E3012:
    strict: true
regions:
  - us-east-1
  - eu-west-1

Policy Compliance with cfn-guard

cfn-guard evaluates CloudFormation templates against policy rules written in Guard's rule language. Unlike cfn-lint (which validates template syntax and structure), cfn-guard enforces organizational policies:

# Install
curl --proto '=https' --tlsv1.2 -sSf \
  https://raw.githubusercontent.com/aws-cloudformation/cloudformation-guard/main/install.sh | sh

# Run rules against a template
cfn-guard validate --data template.yaml --rules rules/

Example rules:

# rules/s3-security.guard

# S3 buckets must have versioning enabled
rule s3_bucket_versioning_required {
  AWS::S3::Bucket {
    Properties.VersioningConfiguration.Status == "Enabled"
  }
}

# S3 buckets must block public access
rule s3_bucket_public_access_blocked {
  AWS::S3::Bucket {
    Properties.PublicAccessBlockConfiguration {
      BlockPublicAcls == true
      BlockPublicPolicy == true
      IgnorePublicAcls == true
      RestrictPublicBuckets == true
    }
  }
}
# rules/encryption.guard

# EBS volumes must be encrypted
rule ebs_encryption_required {
  AWS::EC2::Volume {
    Properties.Encrypted == true
  }
}

# RDS instances must use encrypted storage
rule rds_encryption_required {
  AWS::RDS::DBInstance {
    Properties.StorageEncrypted == true
  }
}

# S3 buckets must have server-side encryption
rule s3_encryption_required {
  AWS::S3::Bucket {
    Properties.BucketEncryption exists
    Properties.BucketEncryption.ServerSideEncryptionConfiguration[*].ServerSideEncryptionByDefault.SSEAlgorithm IN ["aws:kms", "AES256"]
  }
}
# rules/iam-security.guard

# IAM roles must not allow all actions
rule iam_no_wildcard_actions {
  when %iam_policies !empty {
    %iam_policies.Properties.PolicyDocument.Statement[*] {
      Action != "*"
    }
  }
}

# Security groups must not allow all inbound traffic
rule sg_no_unrestricted_ssh {
  AWS::EC2::SecurityGroup {
    Properties.SecurityGroupIngress[*] {
      when IpProtocol == "tcp" && FromPort <= 22 && ToPort >= 22 {
        CidrIp != "0.0.0.0/0"
        CidrIpv6 != "::/0"
      }
    }
  }
}

CI integration:

cfn-guard:
  stage: test
  script:
    - cfn-guard validate --data templates/ --rules rules/ --show-summary all
  allow_failure: false  # Block deployment on policy violations

Integration Testing with TaskCat

TaskCat deploys your CloudFormation templates in real AWS accounts and regions, verifying they actually work. It's the closest you can get to production validation without using production.

Installation:

pip install taskcat

Configuration (.taskcat.yml):

project:
  name: my-infrastructure
  regions:
    - us-east-1
    - us-west-2
    - eu-west-1
  s3_bucket: my-taskcat-bucket  # For artifacts

tests:
  main-test:
    template: templates/main.yaml
    parameters:
      InstanceType: t3.micro
      Environment: test
      DBName: testdb
  
  with-custom-params:
    template: templates/main.yaml
    regions:
      - us-east-1  # Override global regions
    parameters:
      InstanceType: t3.small
      Environment: staging
    
  networking-only:
    template: templates/networking.yaml
    parameters:
      VpcCidr: 10.0.0.0/16

Running TaskCat:

# Run all tests
taskcat test run

# Run specific test
taskcat test run --test-names main-test

# Dry run (lint only, no deployment)
taskcat test run --dry-run

# Keep stacks after test (for debugging failures)
taskcat test run --no-delete

TaskCat deploys the template in each configured region, waits for the stack to reach CREATE_COMPLETE, and then deletes it. If the stack fails to create, the test fails.

Parameter files for sensitive values:

// parameters/main.json
[
  {
    "ParameterKey": "DBPassword",
    "ParameterValue": "$[taskcat_genpass_8]"  // Auto-generate password
  },
  {
    "ParameterKey": "KeyPairName",
    "ParameterValue": "$[taskcat_ssm_/test/keypair-name]"  // SSM parameter
  }
]

TaskCat's parameter overrides support dynamic generation:

  • $[taskcat_genpass_N]: Generate a random N-character password
  • $[taskcat_genaz_N]: Get N availability zones from the deployment region
  • $[taskcat_ssm_/path]: Read from SSM Parameter Store
  • $[taskcat_random-numbers]: Generate random numbers

CI/CD integration:

# GitHub Actions
integration-test:
  needs: [cfn-lint, cfn-guard]
  runs-on: ubuntu-latest
  permissions:
    id-token: write
    contents: read
  steps:
    - uses: actions/checkout@v4
    
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        role-to-assume: arn:aws:iam::123456789:role/TaskCatRole
        aws-region: us-east-1
    
    - name: Run TaskCat tests
      run: |
        pip install taskcat
        taskcat test run

TaskCat IAM role requirements: The role used for TaskCat needs permissions to create the resources in your templates, plus:

  • cloudformation:CreateStack
  • cloudformation:DescribeStacks
  • cloudformation:DeleteStack
  • s3:CreateBucket (for TaskCat's artifact bucket)

Template Testing Strategy

A practical CloudFormation testing strategy uses multiple layers:

Layer 1: Pre-commit (seconds)

# Pre-commit hook
cfn-lint template.yaml
cfn-guard validate --data template.yaml --rules rules/

Layer 2: CI (minutes)

- cfn-lint with JUnit output
- cfn-guard validate all templates
- Unit-level JSON schema validation

Layer 3: Integration (30+ minutes, gated)

- TaskCat deployment in test account
- Post-deployment smoke tests
- Stack deletion verification

CloudFormation-Specific Testing Patterns

Testing outputs: Verify stack outputs exist and have expected values after deployment:

# In post-deployment tests
validate-outputs:
  stage: verify
  script:
    - |
      STACK_NAME="test-stack-$CI_JOB_ID"
      
      # Get specific output
      BUCKET_NAME=$(aws cloudformation describe-stacks \
        --stack-name $STACK_NAME \
        --query "Stacks[0].Outputs[?OutputKey=='BucketName'].OutputValue" \
        --output text)
      
      # Verify the bucket actually exists
      aws s3api head-bucket --bucket $BUCKET_NAME
      
      echo "PASS: S3 bucket $BUCKET_NAME exists"

Testing cross-stack references: When stacks use Fn::ImportValue to reference other stacks, verify the exported values exist:

# Check that all imported values are available
aws cloudformation list-exports --query 'Exports[*].Name' | \
  grep "my-stack-VpcId" || echo "ERROR: Required export not found"

Drift detection testing: Verify your template is the source of truth:

# Detect and report drift from expected state
aws cloudformation detect-stack-drift --stack-name $STACK_NAME
aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id $DETECTION_ID

Testing Nested Stacks

For applications using nested stacks, TaskCat handles them automatically if all templates are in S3. Package nested stacks before testing:

# Package templates (uploads nested stacks to S3 and replaces local refs)
aws cloudformation package \
  --template-file parent-stack.yaml \
  --s3-bucket my-artifacts-bucket \
  --output-template-file packaged-template.yaml

# Now run cfn-lint on the packaged template
cfn-lint packaged-template.yaml

Costs and Account Management

TaskCat deploys real resources that cost money. Manage costs:

  1. Use minimal instance types (t3.micro, db.t3.micro) in test parameters
  2. Short-lived resources: TaskCat deletes stacks after tests, but add on-failure: DO_NOTHING for debugging
  3. Dedicated test accounts: Use separate AWS accounts for TaskCat to contain cost and permissions
  4. Region selection: Test in 2-3 regions rather than all 20+
  5. Schedule intensive tests: Run full TaskCat tests on PR merges, not every commit

The combination of fast linting in pre-commit, policy validation in CI, and deployment testing before production creates a comprehensive safety net for infrastructure changes.

Read more

Crossplane Composition Testing: Unit Testing XRDs and Integration Testing with LocalEnv

Crossplane Composition Testing: Unit Testing XRDs and Integration Testing with LocalEnv

Crossplane extends Kubernetes to manage cloud infrastructure resources — AWS, GCP, Azure, and others — using Kubernetes custom resources. Crossplane Compositions define how high-level platform abstractions (like "a PostgreSQL database with backup enabled") translate into provider-specific resources (like an RDSInstance and an S3Bucket for backups). Testing Crossplane compositions

By HelpMeTest
Start now free