> ## Documentation Index
> Fetch the complete documentation index at: https://launchdarkly.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Terraform provider v2.x to v3.0 migration guide

<View title="Developer" />

<View title="Federal docs" />

<View title="EU docs" />

This topic explains how to upgrade your LaunchDarkly Terraform configuration from v2.x to v3.0.

Version 3.0 changes every nested block to a nested attribute, so configurations written for v2 do not parse after you upgrade the provider. You must rewrite them before your first plan against v3. The provider ships the `migrate-tf-syntax` tool to automate most of the rewrite, and it upgrades your Terraform state automatically on your first apply.

To learn more about v3.0, read the [LaunchDarkly Terraform provider release notes](https://registry.terraform.io/providers/launchdarkly/launchdarkly/latest/docs/guides/v3-release-notes).

Here are the changes between versions 2 and 3 of the Terraform provider:

* [Blocks become nested attributes](#blocks-become-nested-attributes)
* [Single-object attributes use object syntax](#single-object-attributes-use-object-syntax)
* [Keyed collections become maps](#keyed-collections-become-maps)
* [Convert your configuration with `migrate-tf-syntax`](#convert-your-configuration-with-migrate-tf-syntax)
* [Finish the migration by hand](#finish-the-migration-by-hand)
* [How v3 upgrades your state](#how-v3-upgrades-your-state)

## Prerequisites

You need the following things to complete this migration:

* Terraform 1.0 or later
* A v2 configuration that applies cleanly, with an empty plan before you start
* A committed copy of your configuration and state, so you can review the upgrade as a diff

## Blocks become nested attributes

Version 3.0 rebuilds the provider on the Terraform Plugin Framework. Every nested block becomes a nested attribute. You assign a single nested structure with `=`, and you wrap a repeated structure in a list of objects.

Here is the same resource in both syntaxes:

<CodeGroup>
  ```hcl title="Feature flag variations" lines wrap theme={null}
  # v2 block syntax
  resource "launchdarkly_feature_flag" "example" {
    variation_type = "boolean"
    variations { value = "true" }
    variations { value = "false" }
  }

  # v3 nested attribute syntax
  resource "launchdarkly_feature_flag" "example" {
    variation_type = "boolean"
    variations = [
      { value = "true" },
      { value = "false" },
    ]
  }
  ```
</CodeGroup>

This change affects every block in the provider.

## Single-object attributes use object syntax

Most blocks become a list of objects. Attributes that hold exactly one object use object syntax instead, a bare `{ ... }` with no surrounding brackets. These attributes are:

* `client_side_availability` and `defaults` on `launchdarkly_feature_flag`
* `default_client_side_availability` on `launchdarkly_project`
* `fallthrough` on `launchdarkly_feature_flag_environment`
* `approval_settings` on `launchdarkly_environment`, and inside each project environment
* `segment_approval_settings` on `launchdarkly_environment`
* `instructions` on `launchdarkly_flag_trigger`
* `boolean_defaults` on `launchdarkly_flag_templates`

The `migrate-tf-syntax` tool emits this form for you:

<CodeGroup>
  ```hcl title="Single-object attribute" lines wrap theme={null}
  # v2 block syntax
  resource "launchdarkly_feature_flag" "example" {
    client_side_availability {
      using_environment_id = true
      using_mobile_key     = false
    }
  }

  # v3 object syntax (no brackets)
  resource "launchdarkly_feature_flag" "example" {
    client_side_availability = {
      using_environment_id = true
      using_mobile_key     = false
    }
  }
  ```
</CodeGroup>

When you read one of these attributes from a data source, use object access without a list index, for example `data.launchdarkly_feature_flag.x.client_side_availability.using_environment_id`.

## Keyed collections become maps

Four collections whose elements have a natural unique key become maps in v3, so adding, removing, or reordering one entry no longer forces a diff or a destructive plan on its siblings.

The `environments` attribute on `launchdarkly_project` moves from an ordered list to a map keyed by the environment `key`. The `key` attribute also stays inside each object and equals the map key, so references like `launchdarkly_project.example.environments["production"].key` keep working. The `migrate-tf-syntax` tool performs this rewrite for you:

<CodeGroup>
  ```hcl title="Project environments" lines wrap theme={null}
  # v2 block syntax              # v3 map syntax (keyed by env key)
  environments {                 environments = {
    key   = "production"           "production" = {
    name  = "Production"             key   = "production"
    color = "EEEEEE"                 name  = "Production"
  }                                  color = "EEEEEE"
                                   }
                                 }
  ```
</CodeGroup>

The map is authoritative. Terraform deletes any environment you remove from the map on apply, and a project must have at least one environment. To manage a project in Terraform but its environments in the LaunchDarkly user interface (UI), or with [`launchdarkly_environment`](https://registry.terraform.io/providers/launchdarkly/launchdarkly/latest/docs/resources/environment) resources, declare your environments and add `lifecycle { ignore_changes = [environments] }`.

<Warning>
  **Changing an environment key deletes the environment**

  An environment's key serves as the map key. Changing an environment's key deletes that environment, including its SDK keys and all flag targeting, and then creates a new environment.
</Warning>

Reference an environment by its key instead of by index. A v2 interpolation such as `launchdarkly_project.example.environments[0].client_side_id` becomes `launchdarkly_project.example.environments["production"].client_side_id`. The `migrate-tf-syntax` tool does not rewrite these positional references, because auto-editing arbitrary expressions risks corrupting your configuration. It detects them and prints the exact replacement to make, including the resolved key, so the fix is mechanical. To learn more, read [Finish the migration by hand](#finish-the-migration-by-hand).

Three more collections follow the same key-addressed map pattern, and the tool rewrites all of them:

* `custom_properties` on `launchdarkly_feature_flag` becomes a map keyed by the custom property `key`, for example `custom_properties = { "my.key" = { name = ..., value = [...] } }`.
* `role_attributes` on `launchdarkly_team` and `launchdarkly_team_member` becomes a plain map of string lists keyed by the role attribute key, for example `role_attributes = { myAttribute = ["value1", "value2"] }`. This is the same shape the `launchdarkly_team_role_mapping` resource already uses.
* `edges` on the new `launchdarkly_ai_agent_graph` resource is a map keyed by edge key. It is net-new in v3, so no rewrite applies.

## Convert your configuration with `migrate-tf-syntax`

The provider ships `migrate-tf-syntax`, a deterministic command-line tool that rewrites every affected attribute across a directory of `.tf` files. It also updates the attributes that v3 removed. For example, it renames `policy_statements` to `inline_roles` on `launchdarkly_access_token`, and it updates references to renamed data source attributes such as `client_side_availability` on the `launchdarkly_project` data source. It adds the now-required `variations` to boolean flags that omitted them.

To convert a configuration directory:

1. Download the `migrate-tf-syntax` archive for your platform from the [provider release assets](https://github.com/launchdarkly/terraform-provider-launchdarkly/releases), or run the tool with Go. Replace `v3.0.0` with the version you are upgrading to. Here is an example:

<CodeGroup>
  ```bash title="Shell" lines wrap theme={null}
  go run github.com/launchdarkly/terraform-provider-launchdarkly/scripts/migrate-tf-syntax@v3.0.0 \
    -dir ./my-config -direction v2-to-v3 -dry-run
  ```
</CodeGroup>

2. Review the dry-run output. The tool prints each file it intends to change.
3. Run the same command without `-dry-run` to write the changes. Add `-recursive` to convert locally vendored modules in the same pass.
4. Run `terraform fmt` to normalize whitespace.
5. Check any `note:` lines the tool printed. For example, an environment whose `key` is a variable or local becomes a parenthesized map key, such as `(local.env_key) = { ... }`. Confirm the expression is the one you expect.
6. Update the provider version constraint to `~> 3.0` and run `terraform plan`.

## Finish the migration by hand

The tool converts syntax only. Complete these follow-ups yourself:

* Add `variations` by hand only for a flag whose `variation_type` is a non-literal expression, such as a variable or local. The tool cannot resolve those statically, so it warns and skips them. Boolean flags with a literal `variation_type` are handled automatically, and the provider preserves any variation `name` or `description` set outside Terraform when your configuration omits them.
* Rewrite `dynamic` blocks. A `dynamic "variations"` block needs a `for` expression, for example `variations = [for v in var.values : { value = v }]`. The tool warns with the file and resource address, and it leaves the attribute unchanged.
* Upgrade modules sourced from a registry or a git URL. The tool rewrites only files it reaches on disk, so upgrade those modules at their source.
* Rewrite positional references to `launchdarkly_project` environments. The tool converts the `environments` block to a map but does not edit index expressions elsewhere in your configuration. It warns on each one with the exact replacement, for example `environments[0]` becomes `environments["production"]` and `environments[*]` becomes `values(...)`. Apply those edits by hand.
* Review projects that pair `lifecycle { ignore_changes = [environments] }` with standalone `launchdarkly_environment` resources. This v2 pattern keeps working in v3. The `ignore_changes` entry preserves the standalone-managed environments in the authoritative map, so Terraform deletes nothing. If you remove the `ignore_changes` entry, first declare every environment in the project's `environments` map, or the next apply deletes the undeclared ones.

## How v3 upgrades your state

The provider includes a state upgrader for every resource whose state shape changed. On your first apply, the provider migrates your state automatically. You do not edit the state file by hand.

Here is a list of state migrations:

* `launchdarkly_access_token`: Moves `policy_statements` into `inline_roles`, and discards `expire`.
* `launchdarkly_custom_role`: Converts `policy` into `policy_statements`.
* `launchdarkly_feature_flag`: Converts `include_in_snippet` into `client_side_availability`, and re-keys `custom_properties` into a map keyed by property key.
* `launchdarkly_project`: Converts `include_in_snippet` into `default_client_side_availability`, re-keys the ordered `environments` list into a map keyed by environment key, and converts each environment's `approval_settings` to an object.
* `launchdarkly_environment`: Converts `approval_settings` and `segment_approval_settings` to objects.
* `launchdarkly_feature_flag_environment`: Converts `fallthrough` to an object.
* `launchdarkly_flag_trigger`: Converts `instructions` to an object.
* `launchdarkly_flag_templates`: Converts `boolean_defaults` to an object.
* `launchdarkly_team` and `launchdarkly_team_member`: Re-key `role_attributes` into a map of string lists.
* `launchdarkly_metric`: Discards `is_active`, and renames `randomization_units` to `analysis_units`, following the LaunchDarkly API's rename.

## Your first plan after upgrading

Expect a non-empty first plan after you upgrade the provider binary. Version 2 stored empty lists where v3 stores null, so diffs such as `policy_statements = [] -> null` appear once and apply cleanly. You may also see one-time, in-place updates that re-assert values the upgrader normalized away, such as `client_side_availability` or `approval_settings` objects that match the LaunchDarkly API defaults. They apply cleanly and do not recur. A few computed attributes show as known after apply on the first plan, and they resolve on apply. No resource is destroyed or recreated. The follow-up plan is empty.

## What does not change

* Version 3.0 removes no resources and no data sources. Every v2 resource and data source remains available.
* Authentication is unchanged. The `access_token`, `oauth_token`, `api_host`, `http_timeout`, and `max_concurrency` provider settings keep the same names and behavior.

## If you use Crossplane

If you embed this provider through Crossplane's Upjet, the block-to-attribute change alters the generated custom resource definition (CRD) shape, even though the attribute names do not change. We recommend that you test CRD regeneration against v3 before you upgrade.
