> For the complete documentation index, see [llms.txt](https://developer.harness.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://developer.harness.io/continuous-delivery/use-continuous-delivery/provision-infrastructure/terraform-infra/provision-infra-dynamically-with-terraform.md).

# Provision target deployment infrastructure dynamically with Terraform

Provision target deployment infrastructure dynamically using Terraform Plan and Apply steps.

Dynamic provisioning creates the target infrastructure **on demand during pipeline execution**, rather than deploying to pre-existing infrastructure. Configure **Terraform Plan** and **Terraform Apply** steps in the **Environment** section of your **Deploy** stage to run your Terraform scripts, map the outputs to the **Infrastructure Definition**, and deploy your application to the newly provisioned infrastructure.

This topic walks you through configuring dynamic provisioning with Terraform in a deployment pipeline. Go to [Terraform provisioning](/continuous-delivery/use-continuous-delivery/provision-infrastructure/terraform-infra/terraform-provisioning-with-harness.md) to understand provisioning modes and concepts.

***

## What you will learn from this topic

* How to [enable dynamic provisioning](#configure-dynamic-provisioning) in your Deploy stage.
* How to configure the [Terraform Plan step](#configure-terraform-plan-step) to preview infrastructure changes.
* How to configure the [Terraform Apply step](#configure-terraform-apply-step) to provision infrastructure.
* How to [map Terraform outputs](#map-terraform-outputs-to-infrastructure) to your Infrastructure Definition for different deployment types.
* How to configure [Terraform Rollback](#configure-terraform-rollback-step) to revert infrastructure changes on failure.
* How to use [complete pipeline examples](#pipeline-examples) for both dynamic and ad hoc provisioning workflows.

***

## Before you begin

* **Harness project access**: View, Create/Edit, and Execute permissions on Pipelines and Environments. Go to [RBAC in Harness](https://developer.harness.io/docs/platform/role-based-access-control/rbac-in-harness) to configure roles.
* **Terraform or OpenTofu installed on delegate**: Terraform or OpenTofu must be installed on the delegate to execute provisioning commands. Go to [Build custom delegate images with third-party tools](https://developer.harness.io/docs/platform/delegates/install-delegates/build-custom-delegate-images-with-third-party-tools) to install Terraform.
* **Terraform configuration files**: Your Terraform scripts must be accessible via a Harness connector. Go to [Connect to a Git repo](https://developer.harness.io/docs/platform/connectors/code-repositories/connect-to-code-repo) for Git connectors.
* **Harness Secret Manager configured**: Harness encrypts Terraform plan files using a Secret Manager before passing them between the Harness Manager and delegates. Go to [Harness Secrets Manager overview](https://developer.harness.io/docs/platform/secrets/secrets-management/harness-secret-manager-overview) to set up a Secret Manager.

***

## Configure dynamic provisioning

Dynamic provisioning steps are configured in the **Environment** section of a Deploy stage. The steps provision infrastructure and map Terraform outputs to your Infrastructure Definition before deploying your application.

Perform the following steps to enable dynamic provisioning in your Deploy stage:

1. In your Deploy stage, select **Infrastructure**.
2. Specify your Environment and Infrastructure Definition if you have not already done so.

   The type of Infrastructure Definition you select determines what Terraform outputs you will need to map later.
3. In **Dynamic provisioning**, select **Provision your infrastructure dynamically during the execution of your pipeline**.

Harness automatically adds the Terraform Plan, Harness Approval, and Terraform Apply steps. You can customize these steps, but plan, approve, and apply is the most common workflow.

***

## Configure Terraform Plan step

The Terraform Plan step connects Harness to your repository and pulls your Terraform scripts. Go to [Terraform Plan step reference](/continuous-delivery/use-continuous-delivery/provision-infrastructure/terraform-infra/run-a-terraform-plan-with-the-terraform-plan-step.md) for detailed configuration options.

### Basic settings

Perform the following steps to configure basic settings for the Terraform Plan step:

1. In **Name**, enter a name for the step (for example, **plan**).

   Harness creates an Entity Id from the name. This Id is used to reference the step in expressions and by the Terraform Apply step.
2. In **Timeout**, enter how long Harness should wait to complete the step before failing it.
3. Select **Run on Remote Workspace** if you want to execute Terraform commands in a Terraform Cloud or Terraform Enterprise workspace instead of on the Harness Delegate.

   When enabled, Harness triggers a run in your remote workspace and monitors its status. The workspace must be configured with your Terraform configuration and variables. This option is useful when you want to leverage Terraform Cloud features like remote state management, policy enforcement, and cost estimation.
4. In **Command**, select **Apply**. This indicates the step can be used with a Terraform Apply step later.
5. In **Provisioner Identifier**, enter a unique value.

   The Provisioner Identifier links the Terraform Plan step to the Terraform Apply step, allowing the Apply step to inherit the plan configuration. This identifier is project-wide and must be unique across all pipelines in your project.

{% hint style="warning" %}
**Provisioner Identifier scope**

The Provisioner Identifier is project-wide. Use unique identifiers for each pipeline to avoid conflicts. Coordinate with your team to ensure identifiers are not reused across pipelines.
{% endhint %}

6. In **Secret Manager**, select the Secret Manager to use for encrypting and storing the Terraform plan file.

   A Terraform plan is a sensitive file that could be misused if accessed. Harness encrypts the plan file and stores it in your Secret Manager, never passing it as plain text between the Harness Manager and delegates.

   <div data-gb-custom-block data-tag="hint" data-style="info" class="hint hint-info"><p><strong>Secret size limitations</strong></p><p>Some Secret Managers (HashiCorp Vault, Azure Key Vault, AWS Secrets Manager) have maximum secret size limitations. If your Terraform plan exceeds this limit, use a Key Management Service like Google Cloud KMS or AWS KMS instead. Consult your Secret Manager's documentation for size limits.</p></div>

### Connect to Terraform repository

Perform the following steps to connect Harness to your Terraform repository:

1. In **Configuration File Repository**, select **Specify Config File** or select the edit icon.
2. Select the provider where your Terraform files are hosted (GitHub, GitLab, Bitbucket, AWS S3, Artifactory, etc.).
3. Select or create a Connector for your repository. Go to [Connect to a Git repo](https://developer.harness.io/docs/platform/connectors/code-repositories/connect-to-code-repo) to create a connector.
4. Configure the repository settings based on your provider. For example,
   * **Git providers**: Select **Latest from Branch** or **Specific Commit ID** in **Git Fetch Type**. Enter the **Branch** name and **Folder Path** to your Terraform configuration files.
5. Select **Submit**.

### Additional configuration options

The Terraform Plan step supports additional configuration options beyond the basic settings and repository connection. Go to [Terraform Plan step reference](/continuous-delivery/use-continuous-delivery/provision-infrastructure/terraform-infra/run-a-terraform-plan-with-the-terraform-plan-step.md) for detailed information on these settings:

* **Workspace**: Specify a Terraform workspace to isolate state for different environments.
* **Connector Credentials**: Select a connector type to authenticate and pull Terraform child modules from private repositories.
* **Terraform Var Files**: Add inline or remote variable files to pass input variables to your Terraform configuration.
* **Backend Configuration**: Configure remote state backend settings for storing Terraform state files.
* **Targets**: Specify resource addresses to limit the plan to specific modules or resources in your Terraform scripts.
* **Environment Variables**: Set key-value pairs for Terraform environment variables.
* **Export JSON representation of Terraform Plan**: Export the plan output in JSON format for programmatic processing.
* **Export Human Readable representation of Terraform Plan**: Export the plan output in human-readable text format.
* **Store terraform plan on delegate**: Store the Terraform plan file on the delegate instead of in the Secret Manager.
* **Skip state storage**: Skip storing the Terraform state in the backend after planning.
* **Create remote workspace with prefix**: Create a new Terraform Cloud workspace with a specified prefix if it does not exist.
* **Command Line Options**: Enter additional Terraform CLI flags to pass to the plan command.

### Command line options

In **Command Line Options**, enter any additional Terraform CLI flags you want to pass to the `terraform plan` command.

For example, you can use `-parallelism=n` to limit the number of concurrent operations, or `-lock-timeout=DURATION` to specify how long to wait for a state lock. Enter flags without the `terraform plan` prefix (for example, enter `-parallelism=5` not `terraform plan -parallelism=5`).

* Select **Skip Refresh Command** to skip running `terraform refresh` before generating the plan.

  By default, Terraform refreshes the state before planning to detect any changes made outside of Terraform. Enable this option to skip the refresh step, which can speed up plan execution but may result in a plan that does not account for manual changes to your infrastructure. Use this option only when you are certain no manual changes have been made.

***

## Configure approval step

Harness automatically adds a Manual Approval step between the Terraform Plan and Terraform Apply steps. You can customize or remove this step.

Go to [Using Manual Harness Approval Steps](/continuous-delivery/use-continuous-delivery/cd-building-blocks/cd-steps/approvals/using-harness-approval-steps-in-cd-stages.md) or [Using Jira and ServiceNow Approval Steps](/continuous-delivery/use-continuous-delivery/cd-building-blocks/cd-steps/approvals/using-jira-and-service-now-approval-steps-in-cd-stages.md) to configure approval steps.

***

## Configure Terraform Apply step

The Terraform Apply step inherits its configuration from the Terraform Plan step and applies the Terraform plan to provision infrastructure.

Perform the following steps to configure the Terraform Apply step:

1. In **Name**, enter a name for the step (for example, **apply**).
2. In **Timeout**, enter how long Harness should wait to complete the step before failing it.
3. In **Configuration Type**, select **Inherit From Plan**.

   This option tells the Apply step to use the plan generated by the Terraform Plan step with the matching Provisioner Identifier. Harness retrieves the encrypted plan from the Secret Manager and applies it.
4. In **Provisioner Identifier**, enter the **same value** you used in the Terraform Plan step.

   The Provisioner Identifier links the Apply step to the Plan step. This identifier must match exactly so the Apply step can inherit the plan configuration.

{% hint style="info" %}
**Match Provisioner Identifiers**

You must use the **same Provisioner Identifier** in both the Terraform Plan and Terraform Apply steps. If the identifiers do not match, the Apply step will fail because it cannot find the plan to apply.
{% endhint %}

Go to [Terraform Apply step reference](/continuous-delivery/use-continuous-delivery/provision-infrastructure/terraform-infra/run-a-terraform-plan-with-the-terraform-apply-step.md) for detailed configuration options.

***

## Map Terraform outputs to infrastructure

After the Terraform Apply step provisions infrastructure, you must map Terraform outputs to your Infrastructure Definition. The required outputs depend on your deployment type.

Terraform outputs are values exported from your Terraform configuration using `output` blocks. Harness uses these outputs to configure the target infrastructure for your deployment. For example, a Kubernetes deployment requires a connector reference and namespace, while an AWS Lambda deployment requires a region and IAM role.

Go to the deployment-specific documentation for detailed instructions on mapping outputs for your deployment type:

* [Kubernetes infrastructure](/continuous-delivery/use-continuous-delivery/deploy-services-on-different-platforms/kubernetes/define-your-kubernetes-target-infrastructure.md): Also used for Helm, Native Helm, and Kustomize deployments
* [Azure Web Apps](/continuous-delivery/use-continuous-delivery/deploy-services-on-different-platforms/azure/azure-web-apps-tutorial.md)
* [AWS ASG](/continuous-delivery/use-continuous-delivery/deploy-services-on-different-platforms/aws/asg/asg-tutorial.md)
* [AWS ECS](/continuous-delivery/use-continuous-delivery/deploy-services-on-different-platforms/aws/ecs/ecs-deployment-tutorial.md)
* [AWS Lambda](/continuous-delivery/use-continuous-delivery/deploy-services-on-different-platforms/aws/aws-lambda-deployments.md)
* [Google Cloud Functions](/continuous-delivery/use-continuous-delivery/deploy-services-on-different-platforms/google-cloud-functions/google-functions.md)
* [VM deployments using SSH](/continuous-delivery/use-continuous-delivery/deploy-services-on-different-platforms/traditional/ssh-ng.md)
* [Windows VM deployments using WinRM](/continuous-delivery/use-continuous-delivery/deploy-services-on-different-platforms/traditional/win-rm-tutorial.md)

***

## Configure Terraform Rollback step

Harness automatically adds a Terraform Rollback step to the **Rollback** section of your stage when you enable dynamic provisioning. This step ensures that infrastructure changes are reverted if the deployment fails.

The Terraform Rollback step is pre-configured when you enable dynamic provisioning. You can customize its settings if needed.

Perform the following steps to configure the Terraform Rollback step:

1. In **Name**, the default name is **Terraform Rollback**. You can change this if needed.
2. In **Timeout**, enter how long Harness should wait to complete the rollback before failing it.
3. In **Provisioner Identifier**, enter the **same value** you used in the Terraform Plan and Apply steps.

   The Provisioner Identifier determines which Terraform state to roll back. It must match the identifier used in your Plan and Apply steps so Harness knows which provisioned resources to revert.

When rollback happens, Harness rolls back the provisioned infrastructure to the previous successful Terraform state for the Provisioner Identifier. Harness performs a hard rollback to the exact version of the state, without incrementing the serial.

Go to [Terraform Rollback step reference](/continuous-delivery/use-continuous-delivery/provision-infrastructure/terraform-infra/rollback-provisioned-infra-with-the-terraform-rollback-step.md) to configure rollback.

***

## Terraform output expressions

Terraform steps output the commit ID of configuration files stored in Git. You can reference these outputs using Harness expressions in subsequent steps or stages.

For example, for a Terraform Apply step with the identifier `TerraformApply`, you can access the following outputs:

* **Config files**: `<+pipeline.stages.test.spec.execution.steps.TerraformApply.git.revisions.TF_CONFIG_FILES>`
* **Backend config files**: `<+pipeline.stages.test.spec.execution.steps.TerraformApply.git.revisions.TF_BACKEND_CONFIG_FILE>`
* **Var file** with identifier `varfile1`: `<+pipeline.stages.test.spec.execution.steps.TerraformApply.git.revisions.varfile1>`

***

## Pipeline examples

The following examples show complete pipeline YAML for both dynamic and ad hoc provisioning using Terraform.

### Dynamic provisioning example

This example provisions a Kubernetes namespace with resource quotas and limit ranges in the **Environment > provisioner** section, then verifies the provisioned infrastructure in the **Execution** section. The Infrastructure Definition can reference Terraform outputs to deploy to the dynamically provisioned namespace.

<details>

<summary>Dynamic provisioning pipeline YAML</summary>

```yaml
# Terraform Dynamic Provisioning - Complete Working Example
# Provisions Kubernetes namespace with resource quotas and limit ranges
# Demonstrates cleanup, installation, plan, approval, apply, verification, and destroy workflow

pipeline:
  name: TF Dynamic - Working Complete
  identifier: TF_Dynamic_Working_Complete
  projectIdentifier: KrishnamoorthyTest  # Replace with your project
  orgIdentifier: default
  tags: {}
  description: Terraform dynamic provisioning - Complete workflow with Destroy
  
  # Pipeline variables
  variables:
    # Unique identifier linking TerraformPlan, TerraformApply, and TerraformDestroy steps
    - name: provisioner_id
      type: String
      value: tf_working
    # Target Kubernetes namespace to provision
    - name: target_namespace
      type: String
      value: harness-tf-demo
    # Environment type (validated by Terraform: dev/qa/staging/prod)
    - name: environment_name
      type: String
      value: dev
  
  stages:
    - stage:
        name: Provision and Deploy
        identifier: Provision_Deploy
        type: Deployment
        spec:
          deploymentType: Kubernetes
          
          # Service reference
          service:
            serviceRef: PielineK8S_service  # Replace with your service
          
          # Environment configuration with dynamic provisioning
          environment:
            environmentRef: testenv  # Replace with your environment
            deployToAll: false
            
            # PROVISIONER SECTION - Runs BEFORE deployment
            # These steps provision infrastructure and map outputs to Infrastructure Definition
            provisioner:
              steps:
                # Step 1: Cleanup any existing namespace (prevents conflicts)
                - step:
                    type: ShellScript
                    name: Cleanup Existing Namespace
                    identifier: Cleanup_Existing_Namespace
                    spec:
                      shell: Bash
                      onDelegate: true
                      source:
                        type: Inline
                        spec:
                          script: |
                            echo "=========================================="
                            echo "CLEANUP CHECK"
                            echo "=========================================="

                            NS="<+pipeline.variables.target_namespace>"

                            # Check if namespace exists from previous run
                            if kubectl get namespace "$NS" &>/dev/null; then
                              echo "⚠️  WARNING: Namespace '$NS' already exists!"
                              echo ""
                              echo "Deleting namespace and ALL its resources..."
                              kubectl delete namespace "$NS" --grace-period=0 --force
                              
                              echo ""
                              echo "Waiting for complete deletion (up to 2 minutes)..."
                              # Wait up to 2 minutes for namespace deletion
                              for i in {1..60}; do
                                if ! kubectl get namespace "$NS" &>/dev/null 2>&1; then
                                  echo ""
                                  echo "✅ Namespace deleted successfully!"
                                  echo "=========================================="
                                  exit 0
                                fi
                                echo -n "."
                                sleep 2
                              done
                              
                              echo ""
                              echo "❌ ERROR: Namespace still exists after 2 minutes"
                              echo "Manual cleanup required: kubectl delete namespace $NS"
                              echo "=========================================="
                              exit 1
                            else
                              echo "✅ Namespace does not exist - ready to provision"
                              echo "=========================================="
                            fi
                      delegateSelectors:
                        - kubernetes-delegate  # Replace with your delegate selector
                    timeout: 5m
                
                # Step 2: Install Terraform on delegate (if not already installed)
                - step:
                    type: ShellScript
                    name: Install Terraform
                    identifier: Install_Terraform
                    spec:
                      shell: Bash
                      onDelegate: true
                      source:
                        type: Inline
                        spec:
                          script: |
                            # Check if Terraform is already installed
                            if command -v terraform &> /dev/null; then
                              echo "✅ Terraform already installed"
                              terraform --version
                              exit 0
                            fi
                            
                            # Install Terraform 1.5.7
                            cd /tmp
                            TERRAFORM_VERSION="1.5.7"
                            echo "Downloading Terraform ${TERRAFORM_VERSION}..."
                            curl -fsSL "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip" -o terraform.zip
                            
                            # Install unzip if needed
                            if ! command -v unzip &> /dev/null; then
                              microdnf install -y unzip || true
                            fi
                            
                            unzip -o terraform.zip
                            chmod +x terraform
                            
                            # Try to move to standard locations
                            if mv terraform /usr/local/bin/ 2>/dev/null; then
                              echo "✅ Installed to /usr/local/bin/"
                            elif mv terraform /usr/bin/ 2>/dev/null; then
                              echo "✅ Installed to /usr/bin/"
                            else
                              echo "✅ Installed to /tmp/"
                              export PATH=$PATH:/tmp
                            fi
                            
                            terraform --version
                      delegateSelectors:
                        - kubernetes-delegate  # Replace with your delegate selector
                    timeout: 10m
                
                # Step 3: Terraform Plan - Preview infrastructure changes
                - step:
                    type: TerraformPlan
                    name: Terraform Plan
                    identifier: Terraform_Plan
                    spec:
                      # CRITICAL: Links Plan and Apply steps together
                      provisionerIdentifier: <+pipeline.variables.provisioner_id>
                      configuration:
                        command: Apply
                        # Terraform configuration files from Git
                        configFiles:
                          store:
                            type: Github
                            spec:
                              connectorRef: GitHub_Connector  # Replace with your connector
                              gitFetchType: Branch
                              branch: master
                              folderPath: terraform  # Path to Terraform files
                              repoName: harnesscd-example-apps
                        # Pass variables to Terraform
                        varFiles:
                          - varFile:
                              type: Inline
                              identifier: namespace_vars
                              spec:
                                content: |
                                  namespace_name   = "<+pipeline.variables.target_namespace>"
                                  environment_type = "<+pipeline.variables.environment_name>"
                        # Secret Manager for encrypting plan file
                        secretManagerRef: harnessSecretManager
                        # CRITICAL: Store plan on delegate (prevents NullPointerException)
                        storeTfPlanOnDelegate: true
                        # CRITICAL: Skip state storage (prevents version conflicts)
                        skipStateStorage: true
                        skipRefreshCommand: false
                      # CRITICAL: Must match Apply step delegate selector
                      delegateSelectors:
                        - kubernetes-delegate  # Replace with your delegate selector
                    timeout: 10m
                
                # Step 4: Manual approval before provisioning
                - step:
                    type: HarnessApproval
                    name: Approve Terraform Plan
                    identifier: Approve_Plan
                    spec:
                      approvalMessage: |-
                        Review Terraform plan for namespace: <+pipeline.variables.target_namespace>
                        Environment: <+pipeline.variables.environment_name>

                        Approve to proceed with provisioning.
                      includePipelineExecutionHistory: true
                      approvers:
                        userGroups:
                          - account._account_all_users  # Replace with your approver group
                        minimumCount: 1
                        disallowPipelineExecutor: false
                    timeout: 1d
                
                # Step 5: Terraform Apply - Provision infrastructure
                - step:
                    type: TerraformApply
                    name: Terraform Apply
                    identifier: Terraform_Apply
                    spec:
                      # CRITICAL: Must match Plan step provisionerIdentifier
                      provisionerIdentifier: <+pipeline.variables.provisioner_id>
                      configuration:
                        # Inherits plan from Terraform_Plan step
                        type: InheritFromPlan
                      # CRITICAL: Must match Plan step delegate selector (plan file location)
                      delegateSelectors:
                        - kubernetes-delegate  # Replace with your delegate selector
                    timeout: 10m
              
              # Rollback steps - Execute if provisioning fails
              rollbackSteps:
                - step:
                    type: TerraformDestroy
                    name: Rollback Terraform Resources
                    identifier: Terraform_Destroy_Rollback
                    spec:
                      # Must match Apply step provisionerIdentifier
                      provisionerIdentifier: <+pipeline.variables.provisioner_id>
                      configuration:
                        # Inherits state from Terraform_Apply step
                        type: InheritFromApply
                      delegateSelectors:
                        - kubernetes-delegate  # Replace with your delegate selector
                    timeout: 10m
            
            # Infrastructure Definition - References environment-level infrastructure
            # The environment's "dynamicTF" infrastructure is configured with:
            #   namespace: <+provisioner.tf_working.namespace_name>
            #   connectorRef: Krishtest
            #   releaseName: dynamic-<+INFRA_KEY_SHORT_ID>
            infrastructureDefinitions:
              - identifier: dynamicTF  # Replace with your infrastructure identifier
          
          # EXECUTION SECTION - Runs AFTER provisioning
          # These steps execute on the dynamically provisioned infrastructure
          execution:
            steps:
              # Step 1: Display Terraform outputs
              - step:
                  type: ShellScript
                  name: Display All Terraform Outputs
                  identifier: Display_Outputs
                  spec:
                    shell: Bash
                    onDelegate: true
                    source:
                      type: Inline
                      spec:
                        script: |
                          echo "=========================================="
                          echo "TERRAFORM DYNAMIC PROVISIONING OUTPUTS"
                          echo "=========================================="
                          echo ""
                          echo "=== Provisioned Resources ==="
                          echo "Namespace: <+pipeline.variables.target_namespace>"
                          echo "Environment: <+pipeline.variables.environment_name>"
                          echo ""
                          echo "=========================================="
                    delegateSelectors:
                      - kubernetes-delegate  # Replace with your delegate selector
                  timeout: 10m
              
              # Step 2: Verify provisioned resources exist
              - step:
                  type: ShellScript
                  name: Verify Provisioned Resources
                  identifier: Verify_Resources
                  spec:
                    shell: Bash
                    onDelegate: true
                    source:
                      type: Inline
                      spec:
                        script: |
                          echo "=========================================="
                          echo "VERIFYING TERRAFORM RESOURCES"
                          echo "=========================================="

                          NS="<+pipeline.variables.target_namespace>"

                          # Verify namespace exists
                          echo ""
                          echo "1. Checking namespace..."
                          if kubectl get namespace "$NS" &>/dev/null; then
                            echo "✅ Namespace exists: $NS"
                            kubectl get namespace "$NS" --show-labels
                          else
                            echo "❌ Namespace not found: $NS"
                            exit 1
                          fi

                          # Verify resource quota
                          echo ""
                          echo "2. Checking resource quota..."
                          if kubectl get resourcequota -n "$NS" &>/dev/null; then
                            echo "✅ Resource quota exists"
                            kubectl describe resourcequota -n "$NS"
                          else
                            echo "❌ Resource quota not found"
                          fi

                          # Verify limit range
                          echo ""
                          echo "3. Checking limit range..."
                          if kubectl get limitrange -n "$NS" &>/dev/null; then
                            echo "✅ Limit range exists"
                            kubectl describe limitrange -n "$NS"
                          else
                            echo "❌ Limit range not found"
                          fi

                          echo ""
                          echo "=========================================="
                          echo "✅ All resources verified successfully!"
                          echo "=========================================="
                    delegateSelectors:
                      - kubernetes-delegate  # Replace with your delegate selector
                  timeout: 10m
              
              # Step 3: Approve before destroying (optional cleanup step)
              - step:
                  type: HarnessApproval
                  name: Approve Destroy
                  identifier: Approve_Destroy
                  spec:
                    approvalMessage: |-
                      Review provisioned resources:

                      Namespace: <+pipeline.variables.target_namespace>
                      Environment: <+pipeline.variables.environment_name>

                      Approve to DESTROY all provisioned infrastructure.
                    includePipelineExecutionHistory: true
                    approvers:
                      userGroups:
                        - KrishUsergroup  # Replace with your approver group
                      minimumCount: 1
                      disallowPipelineExecutor: false
                    isAutoRejectEnabled: false
                  timeout: 1d
              
              # Step 4: Terraform Destroy (optional cleanup)
              # NOTE: In production, you typically do NOT destroy dynamically provisioned
              # infrastructure after deployment. This step is for demo/testing purposes.
              - step:
                  type: TerraformDestroy
                  name: Terraform Destroy
                  identifier: Terraform_Destroy
                  spec:
                    # Must match Apply step provisionerIdentifier
                    provisionerIdentifier: <+pipeline.variables.provisioner_id>
                    configuration:
                      # Inherits state from Terraform_Apply step
                      type: InheritFromApply
                    delegateSelectors:
                      - kubernetes-delegate  # Replace with your delegate selector
                  timeout: 10m
            
            rollbackSteps: []
        
        # Failure strategy - Rolls back on any error
        failureStrategies:
          - onFailure:
              errors:
                - AllErrors
              action:
                type: StageRollback
```

**Key points:**

* **Provisioner section**: Terraform steps run in `Environment > provisioner` section **before** deployment
* **Infrastructure Definition**: References environment-level infrastructure `dynamicTF` configured with `<+provisioner.tf_working.namespace_name>` expression
* **Critical configurations**:
  * `storeTfPlanOnDelegate: true` - Stores plan file on delegate (prevents Secret Manager API errors)
  * `skipStateStorage: true` - Skips Harness state storage (prevents version conflicts)
  * `provisionerIdentifier` - Links Plan, Apply, and Destroy steps together (must match across all steps)
  * `delegateSelectors` - Must be identical on Plan and Apply steps (ensures plan file accessibility)
* **Terraform outputs**: Available via `<+provisioner.PROVISIONER_ID.OUTPUT_NAME>` in Infrastructure Definition
* **Execution steps**: Verify and display provisioned infrastructure, optional destroy for cleanup

</details>

### Ad hoc provisioning example

This example provisions infrastructure in the **Execution** section as a standalone task without deploying an application.

<details>

<summary>Ad hoc provisioning pipeline YAML</summary>

```yaml
pipeline:
  name: Terraform Ad-Hoc Complete
  identifier: Terraform_AdHoc_Complete
  projectIdentifier: YourProject  # Replace with your project
  orgIdentifier: default
  description: Ad-hoc Terraform pipeline with full lifecycle
  
  # Pipeline variables for Terraform configuration
  variables:
    - name: provisioner_id
      type: String
      value: tf_adhoc  # Unique identifier linking Plan/Apply/Destroy steps
    - name: target_namespace
      type: String
      value: harness-adhoc-demo  # Kubernetes namespace to create
    - name: environment_name
      type: String
      value: dev  # Must match validation in Terraform variables.tf
  
  stages:
    - stage:
        name: Terraform Ad-Hoc
        identifier: Terraform_AdHoc
        type: Deployment
        spec:
          deploymentType: Kubernetes
          service:
            serviceRef: your_service  # Replace with your service
          environment:
            environmentRef: your_environment  # Replace with your environment
            deployToAll: false
            infrastructureDefinitions:
              - identifier: your_infra  # Replace with your infrastructure
                inputs:
                  identifier: your_infra
                  type: KubernetesDirect
                  spec:
                    connectorRef: your_k8s_connector  # Replace with your Kubernetes connector
                    namespace: <+pipeline.variables.target_namespace>
                    releaseName: adhoc-<+INFRA_KEY_SHORT_ID>
                    # IMPORTANT: Links infrastructure to Terraform provisioner outputs
                    provisionerIdentifier: <+pipeline.variables.provisioner_id>
          
          execution:
            steps:
              # STEP 1: Cleanup existing namespace (prevents "already exists" errors)
              - step:
                  type: ShellScript
                  name: Cleanup Existing Namespace
                  identifier: Cleanup_Namespace
                  spec:
                    shell: Bash
                    onDelegate: true
                    source:
                      type: Inline
                      spec:
                        script: |
                          NS="<+pipeline.variables.target_namespace>"
                          if kubectl get namespace "$NS" &>/dev/null; then
                            kubectl delete namespace "$NS" --grace-period=0 --force
                            # Wait up to 2 minutes for deletion
                            for i in {1..60}; do
                              if ! kubectl get namespace "$NS" &>/dev/null 2>&1; then
                                echo "✅ Namespace deleted!"
                                exit 0
                              fi
                              sleep 2
                            done
                            exit 1
                          fi
                    delegateSelectors:
                      - your-delegate-selector  # Replace with your delegate
                  timeout: 5m
              
              # STEP 2: Install Terraform (not pre-installed on delegates)
              - step:
                  type: ShellScript
                  name: Install Terraform
                  identifier: Install_Terraform
                  spec:
                    shell: Bash
                    onDelegate: true
                    source:
                      type: Inline
                      spec:
                        script: |
                          if command -v terraform &> /dev/null; then
                            terraform --version
                            exit 0
                          fi
                          cd /tmp
                          curl -fsSL "https://releases.hashicorp.com/terraform/1.5.7/terraform_1.5.7_linux_amd64.zip" -o terraform.zip
                          if ! command -v unzip &> /dev/null; then
                            microdnf install -y unzip || true
                          fi
                          unzip -o terraform.zip
                          chmod +x terraform
                          mv terraform /usr/local/bin/ || mv terraform /usr/bin/ || export PATH=$PATH:/tmp
                          terraform --version
                    delegateSelectors:
                      - your-delegate-selector  # Same as cleanup step
                  timeout: 10m
              
              # STEP 3: Terraform Plan
              - step:
                  type: TerraformPlan
                  name: Terraform Plan
                  identifier: Terraform_Plan
                  spec:
                    provisionerIdentifier: <+pipeline.variables.provisioner_id>
                    configuration:
                      command: Apply
                      # Terraform files source
                      configFiles:
                        store:
                          type: Github
                          spec:
                            connectorRef: your_github_connector  # Replace
                            gitFetchType: Branch
                            branch: main
                            folderPath: terraform  # Path to .tf files
                            repoName: your-org/your-terraform-repo  # Replace
                      # Inline variable values
                      varFiles:
                        - varFile:
                            type: Inline
                            identifier: namespace_vars
                            spec:
                              content: |
                                namespace_name   = "<+pipeline.variables.target_namespace>"
                                environment_type = "<+pipeline.variables.environment_name>"
                      secretManagerRef: harnessSecretManager
                      # CRITICAL: Store plan on delegate to avoid null pointer in Secret Manager
                      storeTfPlanOnDelegate: true
                      # CRITICAL: Skip state storage to avoid version conflicts
                      skipStateStorage: true
                    # CRITICAL: Must match Apply step to run on same pod
                    delegateSelectors:
                      - your-delegate-selector  # Replace
                  timeout: 10m
              
              # STEP 4: Manual approval before applying
              - step:
                  type: HarnessApproval
                  name: Approve Terraform Plan
                  identifier: Approve_Plan
                  spec:
                    approvalMessage: Review and approve Terraform plan
                    includePipelineExecutionHistory: true
                    approvers:
                      userGroups:
                        - account._account_all_users  # Replace with your approver group
                      minimumCount: 1
                      disallowPipelineExecutor: false
                  timeout: 1d
              
              # STEP 5: Terraform Apply (creates resources)
              - step:
                  type: TerraformApply
                  name: Terraform Apply
                  identifier: Terraform_Apply
                  spec:
                    provisionerIdentifier: <+pipeline.variables.provisioner_id>
                    configuration:
                      type: InheritFromPlan  # Uses plan from Step 3
                    # CRITICAL: Must match Plan step to find plan file on same pod
                    delegateSelectors:
                      - your-delegate-selector  # Same as Plan step
                  timeout: 10m
              
              # STEP 6: Display Terraform outputs
              - step:
                  type: ShellScript
                  name: Display Terraform Outputs
                  identifier: Display_Outputs
                  spec:
                    shell: Bash
                    onDelegate: true
                    source:
                      type: Inline
                      spec:
                        script: |
                          # Ad-hoc provisioning expression format:
                          # <+execution.steps.STEP_ID.output.OUTPUT_NAME>
                          echo "Namespace: <+execution.steps.Terraform_Apply.output.namespace_name>"
                          echo "Quota: <+execution.steps.Terraform_Apply.output.quota_name>"
                          echo "Status: <+execution.steps.Terraform_Apply.output.provisioning_status>"
                  timeout: 10m
              
              # STEP 7: Verify resources with kubectl
              - step:
                  type: ShellScript
                  name: Verify Resources
                  identifier: Verify_Resources
                  spec:
                    shell: Bash
                    onDelegate: true
                    source:
                      type: Inline
                      spec:
                        script: |
                          NS="<+pipeline.variables.target_namespace>"
                          kubectl get namespace "$NS" --show-labels
                          kubectl describe resourcequota -n "$NS"
                          kubectl describe limitrange -n "$NS"
                    delegateSelectors:
                      - your-delegate-selector  # Same delegate
                  timeout: 10m
              
              # STEP 8: Manual approval before destroying
              - step:
                  type: HarnessApproval
                  name: Approve Destroy
                  identifier: Approve_Destroy
                  spec:
                    approvalMessage: Approve to destroy all resources
                    includePipelineExecutionHistory: true
                    approvers:
                      userGroups:
                        - account._account_all_users  # Replace
                      minimumCount: 1
                      disallowPipelineExecutor: false
                  timeout: 1d
              
              # STEP 9: Terraform Destroy (removes all resources)
              - step:
                  type: TerraformDestroy
                  name: Terraform Destroy
                  identifier: Terraform_Destroy
                  spec:
                    provisionerIdentifier: <+pipeline.variables.provisioner_id>
                    configuration:
                      type: InheritFromApply  # Uses state from Apply step
                    delegateSelectors:
                      - your-delegate-selector  # Same as Plan/Apply
                  timeout: 10m
              
              # STEP 10: Success message
              - step:
                  type: ShellScript
                  name: Completion Message
                  identifier: Completion_Message
                  spec:
                    shell: Bash
                    onDelegate: true
                    source:
                      type: Inline
                      spec:
                        script: |
                          echo "✅ Full Terraform lifecycle completed!"
                          echo "  ✓ Cleanup → Install → Plan → Approve → Apply"
                          echo "  ✓ Display → Verify → Approve Destroy → Destroy"
                  timeout: 10m
            
            rollbackSteps: []
        
        failureStrategies:
          - onFailure:
              errors:
                - AllErrors
              action:
                type: StageRollback
```

**Key configurations:**

* `storeTfPlanOnDelegate: true` - Stores plan on delegate to avoid null pointer issues
* `skipStateStorage: true` - Skips Harness state storage to avoid version conflicts
* `delegateSelectors` - Ensures Plan and Apply run on same pod

**Terraform outputs expression:** `<+execution.steps.Terraform_Apply.output.OUTPUT_NAME>`

</details>

***

## Troubleshooting

<details>

<summary>Error acquiring the state lock in Terraform Plan step</summary>

Ensure no other process is holding a lock on the Terraform state. Harness retries state lock acquisition automatically.

</details>

<details>

<summary>Terraform plan file exceeds Secret Manager size limit</summary>

Switch to a Key Management Service like Google Cloud KMS or AWS KMS, which do not have the same size limitations as vault-based Secret Managers.

</details>

<details>

<summary>Terraform module source authentication fails with private Git repositories</summary>

In the Terraform Plan step, enable 'Use Connector credentials' under Source Module and ensure your Harness Git Connector uses SSH or HTTPS authentication.

</details>

<details>

<summary>Provisioner Identifier conflict across multiple pipelines</summary>

Provisioner Identifiers are project-wide. Use unique identifiers for each pipeline or coordinate with your team to avoid reusing identifiers.

</details>

<details>

<summary>NullPointerException in Terraform Plan step after successful terraform init and refresh</summary>

This occurs when the Harness backend fails to encrypt or store the Terraform plan file in the Secret Manager. Enable 'Store terraform plan on delegate' (storeTfPlanOnDelegate: true) in the Terraform Plan step to store the plan on the delegate at /opt/harness-delegate/tf-plan/ instead of in Secret Manager. This bypasses the Secret Manager encryption step.

</details>

<details>

<summary>Unable to update StateFile version for entityId error in Terraform Plan</summary>

This state version conflict occurs when the Provisioner Identifier was used in a previous execution with different state. Enable 'Skip state storage' (skipStateStorage: true) in the Terraform Plan step to skip Harness local state upload and download operations. Alternatively, use a different Provisioner Identifier or clear the existing state for that identifier.

</details>

<details>

<summary>Terraform Apply fails with 'Terraform Plan File not present in local terraform directory'</summary>

This occurs when Terraform Plan and Apply steps run on different delegate pods. When using 'storeTfPlanOnDelegate: true', you must add the same 'delegateSelectors' to both Terraform Plan and Terraform Apply steps to ensure they execute on the same pod where the plan file is stored.

</details>

<details>

<summary>Terraform Apply fails with 'resource already exists' error (e.g., namespace already exists)</summary>

This occurs when resources from a previous execution were not cleaned up. Add a Shell Script step before Terraform Plan to delete existing resources (e.g., kubectl delete namespace), or use a different resource name. For production workflows, implement proper state management with remote backends to track resource ownership.

</details>

<details>

<summary>Infrastructure definition fails with 'Not found provisioner output, provisionerIdentifier: input'</summary>

Your infrastructure definition is configured with 'provisionerIdentifier: <+input>' but the pipeline is not providing this runtime input. Add 'provisionerIdentifier: <+pipeline.variables.provisioner\_id>' under 'infrastructureDefinitions > inputs > spec' in your pipeline YAML to explicitly provide the Provisioner Identifier value.

</details>

<details>

<summary>Terraform command not found on delegate during Terraform Plan step</summary>

Terraform is not pre-installed on Harness delegates by default. Add a Shell Script step before Terraform Plan to install Terraform: download the binary from releases.hashicorp.com, extract it, and move it to /usr/local/bin/ or add it to PATH. Alternatively, use a custom delegate image with Terraform pre-installed.

</details>

***

## Next steps

Explore these related topics for more details on Terraform steps and deployment types.

* [Terraform Plan step reference](/continuous-delivery/use-continuous-delivery/provision-infrastructure/terraform-infra/run-a-terraform-plan-with-the-terraform-plan-step.md): Detailed configuration options for the Terraform Plan step.
* [Terraform Apply step reference](/continuous-delivery/use-continuous-delivery/provision-infrastructure/terraform-infra/run-a-terraform-plan-with-the-terraform-apply-step.md): Detailed configuration options for the Terraform Apply step.
* [Terraform Rollback step reference](/continuous-delivery/use-continuous-delivery/provision-infrastructure/terraform-infra/rollback-provisioned-infra-with-the-terraform-rollback-step.md): Configure rollback for dynamically provisioned infrastructure.
* [Terraform provisioning overview](/continuous-delivery/use-continuous-delivery/provision-infrastructure/terraform-infra/terraform-provisioning-with-harness.md): Understand Terraform provisioning modes and concepts in Harness.

{% @harness-feedback/feedback %}
