---
title: "Terraform Provider"
description: "Manage projects, environments, configs, tags, targeting rules, segments, and webhooks as code with the ConfigDirector Terraform provider"
canonical_url: "https://docs.configdirector.com/integrations/terraform"
---
# Terraform Provider

> Manage projects, environments, configs, tags, targeting rules, segments, and webhooks as code with the ConfigDirector Terraform provider

<div hidden="true" dataTitleIcon="terraform">



</div>

<external-badges package-url="https://registry.terraform.io/providers/ConfigDirector/configdirector/latest/docs" registry="terraform" repo="terraform-provider-configdirector">



</external-badges>

## Introduction

The ConfigDirector Terraform provider allows you to manage projects, environments, configs, tags, targeting rules, segments, and webhooks via Terraform and OpenTofu.

To make the same changes from an AI coding assistant instead, see the [MCP server](https://docs.configdirector.com/integrations/mcp).

Refer to the [official ConfigDirector Terraform provider documentation](https://registry.terraform.io/providers/ConfigDirector/configdirector/latest/docs) for a full guide on all the features available in the provider.

## Example

Here is a minimal example on how to use the provider:

```terraform [main.tf]
terraform {
  required_providers {
    configdirector = {
      source  = "ConfigDirector/configdirector"
      version = "~> 0.6"
    }
  }
}

provider "configdirector" {}

resource "configdirector_project" "example" {
  name = "Targeting Rules Example"
  slug = "targeting-rules-example"
}

resource "configdirector_config" "beta_features" {
  project_id    = configdirector_project.example.id
  key           = "beta-features"
  role          = "flag"
  type          = "boolean"
  initial_value = false
}

resource "configdirector_config_targeting_rules" "beta_features_test" {
  project_id       = configdirector_project.example.id
  config_key       = configdirector_config.beta_features.key
  environment_slug = "test"
  default_value    = "false"

  rules = [
    {
      id     = provider::configdirector::rule_id("beta-features-test-named-testers")
      type   = "conditional"
      order  = 0
      target = "value"
      value  = true
      conditions = [
        {
          id           = provider::configdirector::rule_id("beta-features-test-named-testers-identifier")
          attribute    = "identifier"
          operator     = "is one of"
          targetType   = "text"
          targetValues = ["user-101", "user-202"]
        }
      ]
    }
  ]
}

resource "configdirector_config_targeting_rules" "beta_features_production" {
  project_id       = configdirector_project.example.id
  config_key       = configdirector_config.beta_features.key
  environment_slug = "production"
  default_value    = "false"

  rules = [
    {
      id     = provider::configdirector::rule_id("beta-features-production-rollout")
      type   = "percentage"
      order  = 0
      target = "percentage"
      percentages = [
        {
          id         = provider::configdirector::rule_id("beta-features-production-rollout-on")
          percentage = 10
          value      = true
        },
        {
          id         = provider::configdirector::rule_id("beta-features-production-rollout-off")
          percentage = 90
          value      = false
        },
      ]
    }
  ]
}
```

A condition can also test the [context kind](https://docs.configdirector.com/getting-started/core-concepts#contexts): set `attribute` to `"kind"`, `targetType` to `"text"`, and `operator` to `"equals"`, `"does NOT equal"`, `"is one of"`, or `"is NOT one of"`, with context kinds as the target values. A context sent without a context kind is tested as `user`. For example, this condition matches only devices:

```terraform
{
  id           = provider::configdirector::rule_id("beta-features-test-devices-kind")
  attribute    = "kind"
  operator     = "equals"
  targetType   = "text"
  targetValues = ["device"]
}
```

## Segments

The `configdirector_segment` resource manages a [segment](https://docs.configdirector.com/getting-started/segments), and is available from version 0.3.0 of the provider. `groups` holds the project definition as a list of condition groups, each a list of attribute conditions in the same shape as in targeting rules. `overrides` maps an environment id to that environment's own groups.

The resource owns every override of the segment: leaving `overrides` out, or removing an environment from it, removes that override. `groups` and `overrides` are sent as written and never read back, the same as the `rules` of `configdirector_config_targeting_rules`, so changes made outside Terraform do not show as drift.

A targeting rule uses the segment through a segment condition with `kind = "segment"`, an `operator` of `"in"` or `"not in"`, and the segment's `id` as `segmentId`. Continuing the example above:

```terraform [main.tf]
resource "configdirector_environment" "staging" {
  project_id = configdirector_project.example.id
  name       = "Staging"
  slug       = "staging"
  color      = "purple"
  live       = false
}

resource "configdirector_segment" "beta_testers" {
  project_id = configdirector_project.example.id
  key        = "beta-testers"
  name       = "Beta testers"

  groups = [
    [
      {
        id           = provider::configdirector::rule_id("beta-testers-acme-email")
        attribute    = "traits"
        trait        = "/email"
        operator     = "ends with any of"
        targetType   = "text"
        targetValues = ["@acme.com"]
      }
    ],
    [
      {
        id           = provider::configdirector::rule_id("beta-testers-opted-in")
        attribute    = "traits"
        trait        = "/betaOptIn"
        operator     = "equals"
        targetType   = "boolean"
        targetValues = ["true"]
      },
      {
        id           = provider::configdirector::rule_id("beta-testers-app-version")
        attribute    = "appVersion"
        operator     = ">="
        targetType   = "semver"
        targetValues = ["2.0.0"]
      }
    ]
  ]

  overrides = {
    (configdirector_environment.staging.id) = [
      [
        {
          id           = provider::configdirector::rule_id("beta-testers-staging-named-users")
          attribute    = "identifier"
          operator     = "is one of"
          targetType   = "text"
          targetValues = ["qa-user-1", "qa-user-2"]
        }
      ]
    ]
  }
}

resource "configdirector_config" "new_checkout_flow" {
  project_id    = configdirector_project.example.id
  key           = "new-checkout-flow"
  role          = "flag"
  type          = "boolean"
  initial_value = false
}

resource "configdirector_config_targeting_rules" "new_checkout_flow_production" {
  project_id       = configdirector_project.example.id
  config_key       = configdirector_config.new_checkout_flow.key
  environment_slug = "production"
  default_value    = "false"

  rules = [
    {
      id     = provider::configdirector::rule_id("new-checkout-flow-production-beta-testers")
      type   = "conditional"
      order  = 0
      target = "value"
      value  = true
      conditions = [
        {
          id        = provider::configdirector::rule_id("new-checkout-flow-production-beta-testers-segment")
          kind      = "segment"
          operator  = "in"
          segmentId = configdirector_segment.beta_testers.id
        }
      ]
    }
  ]
}
```

An existing segment is imported by the project's id or slug and the segment's key, for example `terraform import configdirector_segment.beta_testers targeting-rules-example/beta-testers`. The groups and overrides cannot be read back, so the first apply after an import sends whatever the configuration declares.

## Config tags

Declare config assignments by name with `tags` on `configdirector_config`. A managed tag's `.name` reference makes Terraform or OpenTofu wait for that tag's creation or rename:

```terraform [main.tf]
resource "configdirector_tag" "payments" {
  project_id = configdirector_project.example.id
  name       = "Payments / EU & US"
}

resource "configdirector_config" "checkout" {
  project_id    = configdirector_project.example.id
  key           = "checkout-enabled"
  role          = "flag"
  type          = "boolean"
  initial_value = false
  tags          = [configdirector_tag.payments.name]
}
```

You can also assign existing literal names, such as `tags = ["Payments / EU & US"]`. Names are resolved within the config's project using the API's trimming and case-equivalent matching. Spaces, slashes, and punctuation are literal; do not URL-encode them. An unknown name fails with a diagnostic naming the tag and project. Assignments never create tags implicitly. Unknown `.name` references stay unknown during planning until the tag is available.

<table>
<thead>
  <tr>
    <th>
      Configuration
    </th>
    
    <th>
      Effect
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Omit <code>
        tags
      </code>
      
      , or use <code>
        null
      </code>
    </td>
    
    <td>
      Observe names and preserve remote assignments
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tags = []
      </code>
    </td>
    
    <td>
      Clear every assignment and manage an empty selection
    </td>
  </tr>
  
  <tr>
    <td>
      A nonempty set of names
    </td>
    
    <td>
      Manage that selection and correct assignment drift
    </td>
  </tr>
</tbody>
</table>

Omission preserves assignments after import and during unrelated updates. Removing `tags` relinquishes ownership without clearing the selection. Import a config with `<project_id_or_slug>/<key>`, for example `terraform import configdirector_config.checkout targeting-rules-example/checkout-enabled` or the same command with `tofu`. After refresh, omit `tags` to preserve assignments or declare a set to take ownership.

Assignments are unordered and limited to 20 distinct project tags. Equivalent spellings share one stable ID; state retains configured spelling while the assigned tag IDs are unchanged, so casing and order alone do not cause repeated drift. A shared tag deletion detaches its assignments, and a declared name that no longer exists fails on the next write. Tags organize configs without affecting evaluation.

Name resolution requires `tags:read`. Creating a tagged config needs `configs:create`, and changing its tags later needs `config-settings:update`. Provider updates also read the config back, so include `configs:read` for that step. Omitted and empty selections need no catalog lookup. The tag resource separately needs `tags:create`, `tags:update`, and `tags:delete` for the corresponding catalog operations, plus `tags:read` for refresh. Missing scopes produce errors instead of silently skipping assignments.

`data.configdirector_config.<name>.tags` and each config in `data.configdirector_configs.<name>.configs` expose current assigned names under `configs:read` alone. No standalone tag lookup data source is required. The resource and data-source configuration is the same in Terraform and OpenTofu.

Assignment support can deploy once the catalog and assignment APIs are available on every API instance. It does not depend on config table filters or dashboard feature flags.

## Webhooks

The `configdirector_webhook` resource manages a [webhook](https://docs.configdirector.com/integrations/webhooks), and is available from version 0.5.0 of the provider. Pass its `secret` to your receiver so it can [verify each request](https://docs.configdirector.com/integrations/webhooks#verifying-requests):

```terraform [main.tf]
resource "configdirector_webhook" "ci" {
  name        = "CI pipeline"
  url         = "https://example.com/configdirector-webhooks"
  project_ids = [configdirector_project.example.id]
  headers = {
    Authorization = "Bearer ${var.ci_token}"
  }
}

module "ci_receiver" {
  source         = "./ci-receiver"
  signing_secret = configdirector_webhook.ci.secret
}
```

A webhook receives every change event type unless `event_types` lists some, and no alert events unless `alert_kinds` lists some, the same defaults as the dashboard's form. Set `event_types = []` for a webhook that receives only alert events. Leave `project_ids`, `environment_ids`, and `tag_ids` empty to let everything through; [the filters](https://docs.configdirector.com/integrations/webhooks#creating-a-webhook) explain what each one lets through.

`secret` and `headers` are sensitive, so no plan shows them. The secret stays the same across updates. After a rotation on the webhook's page, the next refresh picks up the new secret and the next apply passes it on to your receiver. ConfigDirector never shows a header value again once it is saved, so Terraform notices a header added or removed outside Terraform but not a value changed there.

If ConfigDirector [disables the webhook](https://docs.configdirector.com/integrations/webhooks#when-a-webhook-is-disabled) after repeated failures, the next apply enables it again while `enabled` is `true`, its default.

Import a webhook with its ID, for example `terraform import configdirector_webhook.ci 01a18564-96d6-7026-bb0e-aab59053d921` or the same command with `tofu`. The first apply after an import writes the header values the configuration declares. The API token needs the `webhooks:manage` permission, which only owners and admins can grant.

## OpenTofu

The ConfigDirector provider is also [available in the OpenTofu registry](https://search.opentofu.org/provider/configdirector/configdirector/latest).


## Sitemap

See the full [sitemap](https://docs.configdirector.com/sitemap.md) for all pages.
