Most Terraform tutorials cover the basics — write some HCL, run terraform apply, done. But real day-to-day work with Terraform is about the lifecycle: how you initialize a project, manage workspaces across environments, keep state healthy, and wire it all into a CI/CD pipeline that doesn't break on Friday afternoon.

The core commands

The Terraform lifecycle maps to five main commands you'll run constantly:

  • terraform init — downloads providers, initializes the backend, sets up modules
  • terraform validate — syntax and configuration check without hitting any APIs
  • terraform plan — shows a diff of what will change, the most important step
  • terraform apply — executes the plan
  • terraform destroy — tears everything down (use with care in non-dev environments)

The habit that matters most: always review the plan output before applying. Every plan. Even small ones. The number of times a "one-line change" showed an unexpected 15-resource replacement is not zero.

Backend configuration

For anything beyond local testing, state goes in S3 with DynamoDB locking:

terraform {
  backend "s3" {
    bucket         = "my-tfstate-bucket"
    key            = "infra/prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-state-lock"
    encrypt        = true
  }
}

The DynamoDB table prevents concurrent applies from corrupting state. The bucket should have versioning enabled so you can recover from bad state writes.

Workspace management

Workspaces are the cleanest way to manage multiple environments (dev, staging, prod) from a single configuration. Each workspace gets its own state file under the same backend key prefix.

# Create and switch to a new workspace
terraform workspace new staging
terraform workspace select staging
terraform workspace list

I use a locals block to map workspace names to environment-specific values:

locals {
  env = terraform.workspace

  config = {
    dev     = { instance_type = "t3.small",  min_size = 1 }
    staging = { instance_type = "t3.medium", min_size = 2 }
    prod    = { instance_type = "m5.large",  min_size = 4 }
  }

  instance_type = local.config[local.env].instance_type
  min_size      = local.config[local.env].min_size
}

State file hygiene

State drift is the silent killer of Terraform projects. A few rules I follow:

  • Never edit state files manually. Use terraform state mv or terraform import for surgical changes.
  • Run terraform refresh periodically to sync state with actual infrastructure, especially after any manual console changes.
  • Lock down the S3 state bucket with bucket policies — only CI/CD roles and senior engineers should be able to write to it.
  • Separate state per environment and per major subsystem (networking state, app state, data state) — don't put everything in one monolithic state file.

Integrating with Jenkins CI/CD

A standard Jenkins pipeline for Terraform runs four stages: init, validate, plan, and a manual-approval-gated apply:

pipeline {
  agent any
  environment {
    AWS_DEFAULT_REGION = 'us-east-1'
    TF_WORKSPACE      = "${params.ENVIRONMENT}"
  }
  stages {
    stage('Init') {
      steps { sh 'terraform init -input=false' }
    }
    stage('Validate') {
      steps { sh 'terraform validate' }
    }
    stage('Plan') {
      steps {
        sh 'terraform plan -out=tfplan -input=false'
        sh 'terraform show -no-color tfplan > plan.txt'
        archiveArtifacts 'plan.txt'
      }
    }
    stage('Apply') {
      when { branch 'main' }
      input { message "Review plan.txt — apply to ${params.ENVIRONMENT}?" }
      steps { sh 'terraform apply -input=false tfplan' }
    }
  }
}

The manual approval gate on apply is non-negotiable for production. Save the plan output as a Jenkins artifact so the reviewer can read exactly what will change before approving.

Lifecycle meta-arguments

Terraform's lifecycle block lets you control how specific resources behave during plan/apply:

resource "aws_instance" "app" {
  # ...
  lifecycle {
    create_before_destroy = true   # zero-downtime replacement
    prevent_destroy       = true   # block accidental deletion
    ignore_changes        = [tags] # don't drift-detect tag changes
  }
}

prevent_destroy is especially useful on databases and stateful resources where an accidental destroy would be catastrophic.

Wrapping up

The Terraform lifecycle is straightforward once the patterns click — consistent workspace discipline, healthy state management, and a CI/CD pipeline that enforces review before apply. Get those three right and Terraform becomes a reliable, low-drama part of your infrastructure workflow.