Automating Terraform Deployments with Jenkins, AWS, and Slack Notifications

Sagar Veeranna Shiva
NEW IT Engineering
Published in
4 min readAug 15, 2024

Introduction

In modern DevOps practices, managing infrastructure as code (IaC) is essential for maintaining scalable, reliable, and consistent environments. Combining tools like Jenkins, Terraform, and AWS offers a powerful way to automate and control infrastructure deployment workflows.

This blog will walk you through building a Jenkins pipeline that not only automates Terraform runs but also integrates Slack for notification and manual approval of changes. The best part? You can schedule this pipeline to run weekly and notify you when changes are detected

Why Automate with Jenkins and Terraform?

When working with Terraform, you often deal with multiple environments, configurations, and cloud resources. A fully automated pipeline can:

  • Save time by detecting and notify the team to take action immediately.
  • Consistency: Ensures that changes are applied uniformly across environments.
  • Ensure stability by integrating manual approval for critical changes.
  • Improve collaboration with real-time Slack notifications and approval prompts.

With this setup, your team gets the best of both worlds: automation for routine tasks and control for sensitive changes.

Setting Up the Jenkins Pipeline

1. Choosing the Right Jenkins Agent

First, you need a Jenkins agent with both AWS CLI and Terraform pre-installed. For simplicity, we’ll use a public Docker image:

agent {
docker {
image 'mesosphere/aws-cli-terraform:latest'
alwaysPull true
args '--entrypoint='
label 'docker-builder'
}
}

This pre-built image saves you the hassle of building a custom Docker image.

2. Building the Pipeline

Let’s break down the Jenkins pipeline into its key components. Here’s the full pipeline script:

pipeline {
agent {
docker {
image 'mesosphere/aws-cli-terraform:latest'
alwaysPull true
args '--entrypoint='
label 'docker-builder'
}
}
environment {
BASE_TERRAFORM_DIR = "${WORKSPACE}/terraform"
}
triggers {
cron('H 10 * * 1') // Runs every Monday at 10:00 AM
}
stages {
stage('Checkout') {
steps {
script {
// Ensure the BASE_TERRAFORM_DIR exists
sh "mkdir -p ${env.BASE_TERRAFORM_DIR}"

// Check out the repository into the BASE_TERRAFORM_DIR
dir(env.BASE_TERRAFORM_DIR) {
checkout([
$class: 'GitSCM',
branches: [[name: '*/main']], // Replace 'main' with your branch name
userRemoteConfigs: [[url: 'https://github.com/your-repo.git']]
])
}
}
}
}
stage('Detect Terraform Directories') {
steps {
script {
def directories = findTerraformDirectories()
runTerraformStages(directories)
}
}
}
}
post {
always {
cleanWs() // Clean workspace at the end
}
}
}

// Helper functions for Terraform and Slack operations

def findTerraformDirectories() {
sh "find ${env.BASE_TERRAFORM_DIR} -name '*.tf' > terraform_files.txt"
def tfFiles = readFile('terraform_files.txt').trim().split('\n')
return tfFiles.collect { new File(it).getParent() }.unique()
}

def runTerraformStages(directories) {
def parallelStages = [:]
directories.each { dir ->
def folderName = dir.split('/').last()
parallelStages["Terraform Init and Plan: ${folderName}"] = {
stage("Terraform Init and Plan: ${folderName}") {
steps {
script {
withAWS {
runTerraformInitAndPlan(dir, folderName)
}
}
}
}
}
}
parallel parallelStages
}

def runTerraformInitAndPlan(dir, folderName) {
def planCommand = "terraform plan -detailed-exitcode"
def tfPlanOutput = sh(script: """
cd '${dir}'
terraform init
${planCommand}
""", returnStatus: true)

if (tfPlanOutput == 2) {
currentBuild.result = 'UNSTABLE'
sendSlackNotification(folderName)
promptUserForApproval(dir, folderName)
} else if (tfPlanOutput == 1) {
error "Terraform plan failed for ${folderName}."
} else {
echo "No changes detected in ${folderName}."
}
}

def sendSlackNotification(folderName) {
slackSend(
channel: 'automation-team', // Replace with the your Slack channel name
color: '#FF0000',
message: "*🚨 Terraform Changes Detected*\n*Folder*: `${folderName}`\n<${env.BUILD_URL}input|Click here to approve ✅ / Abort ❌>"
)
}

def promptUserForApproval(dir, folderName) {
timeout(time: 30, unit: 'MINUTES') {
def userInput = input message: 'Do you want to apply this plan?',
parameters: [choice(name: 'action', choices: 'Approve ✅\nAbort ❌', description: 'Choose action')]
if (userInput == 'Approve ✅') {
sh """
cd '${dir}'
terraform apply -auto-approve
"""
} else {
error "User aborted the apply process."
}
}
}

def withAWS(closure) {
withCredentials([
string(credentialsId: 'aws-access-key-id', variable: 'AWS_ACCESS_KEY_ID'),
string(credentialsId: 'aws-secret-access-key', variable: 'AWS_SECRET_ACCESS_KEY')
]) {
closure()
}
}

3. Breaking Down the Pipeline

  • Checkout Stage: Clones the repo and sets up your workspace.
  • Detect Terraform Directories: Dynamically identifies all directories containing .tf files and creates parallel stages.
  • Terraform Init and Plan: Runs terraform init and terraform plan, sending a Slack notification if changes are detected.

4. Integrating Slack Notifications

To send Slack notifications, you’ll need the Slack Notification Plugin. This plugin allows Jenkins to post messages to Slack channels or direct messages, keeping your team informed about build statuses and other important updates.

Setting Up the Slack Plugin:

  1. Install the Plugin:
  • Navigate to Manage Jenkins > Manage Plugins.
  • Search for Slack Notification Plugin and install it.

2. Configure Slack Integration:

  • Go to Manage Jenkins > Configure System.
  • Scroll down to Slack Notifications and provide your Slack workspace credentials. You’ll need a Slack OAuth token and the default channel for notifications.

Using Slack in Your Pipeline:

Here’s how you integrate Slack notifications into your pipeline:

sendSlackNotification(folderName) {
slackSend(
channel: 'automation-team', // Replace with your Slack channel name
color: '#FF0000',
message: "*🚨 Terraform Changes Detected*\n*Folder*: `${folderName}`\n<${env.BUILD_URL}input|Click here to approve ✅ / Abort ❌>"
)
}

This message includes a direct link for manual approval.

5. Handling User Input

The pipeline waits for 30 minutes for user input before automatically timing out:

promptUserForApproval(dir, folderName) {
timeout(time: 30, unit: 'MINUTES') {
def userInput = input message: 'Do you want to apply this plan?',
parameters: [choice(name: 'action', choices: 'Approve ✅\nAbort ❌', description: 'Choose action')]
if (userInput == 'Approve ✅') {
sh """
cd '${dir}'
terraform apply -auto-approve
"""
} else {
error "User aborted the apply process."
}
}
}

Conclusion

This Jenkins pipeline provides a robust solution for automating Terraform deployments while integrating manual approval steps and Slack notifications. This setup ensures that your infrastructure changes are reviewed and approved before being applied, maintaining both efficiency and control.

--

--

Sagar Veeranna Shiva
NEW IT Engineering

Senior Devops Engineer, AWS certified developer associate, interested in DevOps, IoT, and Robotics