Terraform Cloud in Jenkins pipeline

Hi,

I have some questions on Terraform Cloud integration using Jenkins + Terraform remote backend.

  1. Is there a sample Jenkinsfile or best practice?
  2. How do I disable automatic workspace runs in Terraform Cloud when I make code commits to Git repo?
  3. How do I run a Jenkins pipeline with:
    - stage1: terraform plan in Jenkins,
    - stage2: wait for manual approval in TC,
    - stage3: terraform apply in Jenkins?
  4. How do I run a speculative plan in PRs - Is it just terraform plan using the remote backend?

Hello!

  1. There are no sample Jenkinsfiles (yet! hoping to get to generating some) but there is a blog on how to use Jenkins with Terraform Enterprise. You can likely find material for Jenkins + Terraform Enterprise and the same approaches will apply to Terraform Cloud. Generally, when using TFC with CI frameworks, you will want to set the workspace to execute remotely while the CI framework integrates with VCS. As a result, you would call terraform plan and terraform apply within your Jenkinsfile with remote execution on Terraform Cloud (no VCS integration set up).

  2. At this time, we do not have a way to disable workspace runs per commit (something like [ci skip], for example). You can disable workspace runs for all commits by resetting the trigger to a different directory upon changes or removing the VCS integration and setting up the CI Framework as outlined above.

  3. To do this, you’ll need to set up TFC with remote plan, no VCS integration. The backend configuration for the workspace must be a file specified by Jenkins or version control.
    There are 2 approaches:

    • Approach 1, Jenkins controls who approves
      • Stage 1: Jenkins runs terraform plan, remote plan by Terraform Cloud.
      • Stage 2: Jenkins input waits for manual approval
      • Stage 3: Jenkins runs terraform apply -auto-approve, remote apply by Terraform Cloud.
    • Approach 2, TFC controls who approves
      • Stage 1: Jenkins runs terraform apply, remote apply by Terraform. This will hang until there is approval in Terraform Cloud. Be sure to set the timeout here to a long timeout for the job.
      • Stage 2: Go into TFC, manually approve. This will continue the Jenkins job and apply the changes.
  4. Yes, triggering a speculative plan in PRs would be a terraform plan with the remote backend.

Hope this is somewhat helpful!

1 Like

Hope this can be of some use… here’s the relevant bits of a Jenkinsfile that uses local apply (not remote apply) that can be used with the TFC remote state backend.

It’s a declarative Jenkinsfile for use with multibranch pipeline jobs. When there’s a new commit to a branch a plan is run and if there are any pending changes it will prompt for approval to run an apply, similar to how it was described above.

Couple things worth noting are the use of the milestone and lockable resources features in the pipeline to resolve concurrency issues if say, multiple devs push stuff within a short amount of time. Any superseded pipeline runs are cancelled and in the case you hit a lock, other waiting (newer) runs will wait for this one to complete.

pipeline {
  agent {
    ...
  }
  options {
    ansiColor('xterm')
    timestamps()
  }
  tools {
    type: 'org.jenkinsci.plugins.terraform.TerraformInstallation' 'terraform'
  }
  environment {
    TF_HOME = tool name: 'terraform', type: 'org.jenkinsci.plugins.terraform.TerraformInstallation'
    PATH = "$TF_HOME:$PATH"
    
    TF_VAR_some_sensitive_var_from_vault = vault path: "/kv/jenkins/path/to/secret", key: 'key'
    ... some other vars ...
  }
  stages {
    stage('setup') {
      steps {
        container('executor') {
          milestone 1
          ...some other setup stuff such as third party providers...
          sh 'terraform version'
        }
      }
    }

    stage('init') {
      when {
        // use named env branches, not master
        not {
          branch 'master'
        }
      }
      environment {
        VAULT_ADDR = '...'
        VAULT_CACERT = 'path/to/vault-ca.pem'
      }
      steps {
        container('executor') {
          milestone 2
          lock(resource: env.BRANCH_NAME, inversePrecedence: true) {
            sshagent(['jenkins-ssh']) {
              sh 'terraform init'
            }
            // each non-master branch represents a desired environment -- we separate the state
            // of these environments using Terraform workspaces. If the workspace doesn't already
            // exist in the state, this is a new environment we're bootstrapping, so we create it.
            sh 'terraform workspace select ${BRANCH_NAME} || terraform workspace new ${BRANCH_NAME}'
            configFileProvider([configFile(fileId: 'vault-ca-cert', targetLocation: env.VAULT_CACERT)]) {
              script {
                def status = sh(script: '''
                  ... some stuff we have for kubernetes auth ...
                  terraform plan -var-file "${BRANCH_NAME}.tfvars" -detailed-exitcode
                ''', returnStatus: true)
                env.WANT_APPLY = status
                if (status == 1) {
                  error("Failed to execute Terraform plan")
                }
              }
            }
          }
        }
      }
    }
    stage('apply') {
      when {
        allOf {
          not {
            // use named env branches, not master
            branch 'master'
          }
          environment name: 'WANT_APPLY', value: '2'
        }
      }
      environment {
        VAULT_ADDR = '...'
        VAULT_CACERT = 'path/to/vault-ca.pem'
      }
      steps {
        container('executor') {
          milestone 3
          input message: 'Review plan and approve to proceed with apply. Ok to apply?'
          lock(resource: env.BRANCH_NAME, inversePrecedence: true) {
            configFileProvider([configFile(fileId: 'vault-ca-cert', targetLocation: env.VAULT_CACERT)]) {
              // each non-master branch represents a desired environment -- we separate the state
              // of these environments using Terraform workspaces. If the workspace doesn't already
              // exist in the state, this is a new environment we're bootstrapping, so we create it.
              sh 'terraform workspace select ${BRANCH_NAME} || terraform workspace new ${BRANCH_NAME}'
              // the detailed exitcode option lets us finish without prompting the user if there are
              // no changes to apply.
              milestone 4
              sh '''
                ... some stuff we have for kubernetes auth ...
                terraform apply -var-file "${BRANCH_NAME}.tfvars" -auto-approve
              '''
            }
          }
        }
      }
    }
  }
}