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 junitCommon 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 defaultIntegrating 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.xmlConfiguration 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-1Policy 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 violationsIntegration 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 taskcatConfiguration (.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/16Running 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-deleteTaskCat 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 runTaskCat IAM role requirements: The role used for TaskCat needs permissions to create the resources in your templates, plus:
cloudformation:CreateStackcloudformation:DescribeStackscloudformation:DeleteStacks3: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 validationLayer 3: Integration (30+ minutes, gated)
- TaskCat deployment in test account
- Post-deployment smoke tests
- Stack deletion verificationCloudFormation-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_IDTesting 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.yamlCosts and Account Management
TaskCat deploys real resources that cost money. Manage costs:
- Use minimal instance types (
t3.micro,db.t3.micro) in test parameters - Short-lived resources: TaskCat deletes stacks after tests, but add
on-failure: DO_NOTHINGfor debugging - Dedicated test accounts: Use separate AWS accounts for TaskCat to contain cost and permissions
- Region selection: Test in 2-3 regions rather than all 20+
- 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.