Testing and CI/CD Integration with Terraform
/ 5 min read
Series Navigation
- Part 1: Terraform Fundamentals
- Part 2: Resource Management and State
- Part 3: Essential Terraform Functions
- Part 4: Variables, Outputs, and Dependencies
- Part 5: Terraform Modules and Workspace Management
- Part 6: Managing Remote State and Backend
- Part 7: Testing and CI/CD Integration (Current)
- Part 8: Terraform Security and Best Practices
Testing Strategies
Unit Testing with Terratest
package test
import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert")
func TestVPCCreation(t *testing.T) { terraformOptions := &terraform.Options{ TerraformDir: "../modules/vpc", Vars: map[string]interface{}{ "vpc_cidr": "10.0.0.0/16", "environment": "test", "region": "us-west-2", }, }
defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions)
vpcID := terraform.Output(t, terraformOptions, "vpc_id") assert.NotEmpty(t, vpcID)}Integration Testing
func TestFullStackDeployment(t *testing.T) { terraformOptions := &terraform.Options{ TerraformDir: "../environments/test", Vars: map[string]interface{}{ "environment": "test", "region": "us-west-2", }, }
defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions)
// Test VPC vpcID := terraform.Output(t, terraformOptions, "vpc_id") assert.NotEmpty(t, vpcID)
// Test Subnets privateSubnets := terraform.Output(t, terraformOptions, "private_subnet_ids") assert.Greater(t, len(privateSubnets), 0)
// Test Load Balancer lbDNS := terraform.Output(t, terraformOptions, "lb_dns_name") assert.NotEmpty(t, lbDNS)}Static Analysis
plugin "aws" { enabled = true version = "0.21.1" source = "github.com/terraform-linters/tflint-ruleset-aws"}
rule "terraform_deprecated_index" { enabled = true}
rule "terraform_unused_declarations" { enabled = true}
rule "terraform_documented_variables" { enabled = true}CI/CD Pipeline Integration
GitHub Actions Workflow
name: Terraform CI/CD
on: push: branches: [ main ] pull_request: branches: [ main ]
jobs: terraform: runs-on: ubuntu-latest
steps: - uses: actions/checkout@v2
- name: Setup Terraform uses: hashicorp/setup-terraform@v1 with: terraform_version: 1.0.0
- name: Terraform Format run: terraform fmt -check
- name: Terraform Init run: terraform init
- name: Terraform Validate run: terraform validate
- name: Terraform Plan run: terraform plan env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Terraform Apply if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: terraform apply -auto-approve env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}GitLab CI Pipeline
image: name: hashicorp/terraform:1.0.0 entrypoint: [""]
variables: TF_ROOT: ${CI_PROJECT_DIR}/environments/prod TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/prod
stages: - validate - plan - apply
before_script: - cd ${TF_ROOT} - terraform init
validate: stage: validate script: - terraform validate - terraform fmt -check
plan: stage: plan script: - terraform plan -out=plan.tfplan artifacts: paths: - plan.tfplan
apply: stage: apply script: - terraform apply plan.tfplan dependencies: - plan only: - main when: manualJenkins Pipeline
// Jenkinsfilepipeline { agent any
environment { TF_IN_AUTOMATION = 'true' AWS_CREDENTIALS = credentials('aws-credentials') }
stages { stage('Checkout') { steps { checkout scm } }
stage('Terraform Init') { steps { sh 'terraform init' } }
stage('Terraform Format') { steps { sh 'terraform fmt -check' } }
stage('Terraform Validate') { steps { sh 'terraform validate' } }
stage('Terraform Plan') { steps { withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'aws-credentials', accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { sh 'terraform plan -out=tfplan' } } }
stage('Terraform Apply') { when { branch 'main' } steps { withCredentials([[$class: 'AmazonWebServicesCredentialsBinding', credentialsId: 'aws-credentials', accessKeyVariable: 'AWS_ACCESS_KEY_ID', secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']]) { sh 'terraform apply -auto-approve tfplan' } } } }
post { always { cleanWs() } }}Automated Testing Setup
Test Structure
.├── modules/│ └── vpc/│ ├── main.tf│ └── test/│ ├── vpc_test.go│ └── integration_test.go├── environments/│ ├── dev/│ │ └── main.tf│ └── prod/│ └── main.tf└── test/ ├── go.mod ├── go.sum └── helper/ └── test_helper.goTest Helper Functions
package helper
import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform")
func SetupTestEnvironment(t *testing.T, terraformDir string) *terraform.Options { terraformOptions := &terraform.Options{ TerraformDir: terraformDir, Vars: map[string]interface{}{ "environment": "test", "region": "us-west-2", }, NoColor: true, }
terraform.Init(t, terraformOptions) return terraformOptions}
func CleanupTestEnvironment(t *testing.T, terraformOptions *terraform.Options) { terraform.Destroy(t, terraformOptions)}Continuous Deployment Strategies
Blue-Green Deployment
variable "environment" { type = string}
variable "color" { type = string}
resource "aws_autoscaling_group" "app" { name = "${var.environment}-${var.color}"
launch_template { id = aws_launch_template.app.id }
target_group_arns = [aws_lb_target_group.app.arn]
tag { key = "Environment" value = var.environment propagate_at_launch = true }
tag { key = "Color" value = var.color propagate_at_launch = true }}
resource "aws_lb_listener_rule" "app" { listener_arn = aws_lb_listener.front_end.arn
action { type = "forward" target_group_arn = aws_lb_target_group.app.arn }
condition { host_header { values = ["${var.color}.${var.environment}.example.com"] } }}Canary Deployment
resource "aws_lambda_alias" "app" { name = "production" function_name = aws_lambda_function.app.function_name function_version = aws_lambda_function.app.version
routing_config { additional_version_weights = { "${aws_lambda_function.app_new.version}" = var.canary_weight } }}
resource "aws_cloudwatch_metric_alarm" "errors" { alarm_name = "lambda-errors" comparison_operator = "GreaterThanThreshold" evaluation_periods = "2" metric_name = "Errors" namespace = "AWS/Lambda" period = "300" statistic = "Sum" threshold = "1"
dimensions = { FunctionName = aws_lambda_function.app_new.function_name Resource = aws_lambda_function.app_new.function_name }
alarm_actions = [aws_sns_topic.rollback.arn]}Monitoring and Alerting
CloudWatch Integration
resource "aws_cloudwatch_dashboard" "terraform" { dashboard_name = "terraform-deployment"
dashboard_body = jsonencode({ widgets = [ { type = "metric" width = 12 height = 6
properties = { metrics = [ ["AWS/States", "ExecutionsSucceeded"], ["AWS/States", "ExecutionsFailed"] ] period = 300 stat = "Sum" region = "us-west-2" title = "Terraform State Machine Executions" } } ] })}
resource "aws_cloudwatch_metric_alarm" "deployment_failure" { alarm_name = "terraform-deployment-failure" comparison_operator = "GreaterThanThreshold" evaluation_periods = "1" metric_name = "ExecutionsFailed" namespace = "AWS/States" period = "300" statistic = "Sum" threshold = "0"
alarm_actions = [aws_sns_topic.alerts.arn]}Next Steps
In Part 8: Terraform Security and Best Practices, we’ll explore security considerations and best practices for Terraform deployments.