TerraformPilot

Terraform

Terraform CI/CD with Jenkins - Complete Pipeline

Automate Terraform with Jenkins pipelines. Declarative and scripted pipelines, credentials management, approval gates, and multi-environment deployments.

LLuca Berton1 min read

Quick Answer

#
pipeline {
  agent { docker { image 'hashicorp/terraform:1.8' } }
  stages {
    stage('Init')  { steps { sh 'terraform init' } }
    stage('Plan')  { steps { sh 'terraform plan -out=tfplan' } }
    stage('Apply') {
      steps {
        input message: 'Apply changes?'
        sh 'terraform apply -auto-approve tfplan'
      }
    }
  }
}

Production Jenkinsfile

#
pipeline {
  agent {
    docker {
      image 'hashicorp/terraform:1.8'
      args '-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_DEFAULT_REGION'
    }
  }
 
  parameters {
    choice(name: 'ENVIRONMENT', choices: ['dev', 'staging', 'prod'], description: 'Target environment')
    booleanParam(name: 'AUTO_APPLY', defaultValue: false, description: 'Skip manual approval')
  }
 
  environment {
    TF_IN_AUTOMATION = 'true'
    TF_VAR_FILE     = "${params.ENVIRONMENT}.tfvars"
  }
 
  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }
 
    stage('Init') {
      steps {
        sh "terraform init -backend-config=backends/${params.ENVIRONMENT}.hcl"
      }
    }
 
    stage('Validate') {
      steps {
        sh 'terraform validate'
        sh 'terraform fmt -check'
      }
    }
 
    stage('Plan') {
      steps {
        sh "terraform plan -var-file=${TF_VAR_FILE} -out=tfplan -no-color"
      }
    }
 
    stage('Approval') {
      when {
        allOf {
          expression { params.ENVIRONMENT == 'prod' }
          expression { !params.AUTO_APPLY }
        }
      }
      steps {
        input message: "Apply to ${params.ENVIRONMENT}?", ok: 'Deploy'
      }
    }
 
    stage('Apply') {
      when {
        branch 'main'
      }
      steps {
        sh 'terraform apply -auto-approve tfplan'
      }
    }
  }
 
  post {
    always {
      archiveArtifacts artifacts: 'tfplan', allowEmptyArchive: true
    }
    failure {
      echo "Terraform failed for ${params.ENVIRONMENT}"
      // slackSend channel: '#infra', message: "❌ Terraform ${params.ENVIRONMENT} failed"
    }
    success {
      echo "Terraform applied to ${params.ENVIRONMENT}"
    }
  }
}

Credentials Management

#
stage('Init') {
  steps {
    withCredentials([
      [$class: 'AmazonWebServicesCredentialsBinding',
       credentialsId: 'aws-terraform',
       accessKeyVariable: 'AWS_ACCESS_KEY_ID',
       secretKeyVariable: 'AWS_SECRET_ACCESS_KEY']
    ]) {
      sh 'terraform init'
      sh 'terraform plan -out=tfplan'
    }
  }
}

Shared Library

#
// vars/terraform.groovy
def call(Map config = [:]) {
  def env = config.environment ?: 'dev'
  
  stage('Terraform Init') {
    sh "terraform init -backend-config=backends/${env}.hcl"
  }
  stage('Terraform Plan') {
    sh "terraform plan -var-file=${env}.tfvars -out=tfplan"
  }
  if (env == 'prod') {
    stage('Approval') {
      input message: "Apply to production?"
    }
  }
  stage('Terraform Apply') {
    sh 'terraform apply -auto-approve tfplan'
  }
}
 
// Jenkinsfile (using shared library)
@Library('terraform-lib') _
terraform(environment: 'prod')
#

Conclusion

#

Use declarative pipelines with Docker agents for consistent Terraform versions, Jenkins credentials for secrets, manual input gates for production, and shared libraries for reusable pipeline code. Always archive the plan artifact and add post-failure notifications.

#Terraform#Jenkins#CI/CD#DevOps#Infrastructure as Code

Share this article