Terraform provider v2.x to v3.0 migration guide

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.

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

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:

Feature flag variations
1# v2 block syntax
2resource "launchdarkly_feature_flag" "example" {
3 variation_type = "boolean"
4 variations { value = "true" }
5 variations { value = "false" }
6}
7
8# v3 nested attribute syntax
9resource "launchdarkly_feature_flag" "example" {
10 variation_type = "boolean"
11 variations = [
12 { value = "true" },
13 { value = "false" },
14 ]
15}

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:

Single-object attribute
1# v2 block syntax
2resource "launchdarkly_feature_flag" "example" {
3 client_side_availability {
4 using_environment_id = true
5 using_mobile_key = false
6 }
7}
8
9# v3 object syntax (no brackets)
10resource "launchdarkly_feature_flag" "example" {
11 client_side_availability = {
12 using_environment_id = true
13 using_mobile_key = false
14 }
15}

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:

Project environments
1# v2 block syntax # v3 map syntax (keyed by env key)
2environments { environments = {
3 key = "production" "production" = {
4 name = "Production" key = "production"
5 color = "EEEEEE" name = "Production"
6} color = "EEEEEE"
7 }
8 }

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 resources, declare your environments and add lifecycle { ignore_changes = [environments] }.

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.

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.

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, or run the tool with Go. Replace v3.0.0 with the version you are upgrading to. Here is an example:
Shell
$go run github.com/launchdarkly/terraform-provider-launchdarkly/scripts/migrate-tf-syntax@v3.0.0 \
> -dir ./my-config -direction v2-to-v3 -dry-run
  1. Review the dry-run output. The tool prints each file it intends to change.
  2. Run the same command without -dry-run to write the changes. Add -recursive to convert locally vendored modules in the same pass.
  3. Run terraform fmt to normalize whitespace.
  4. 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.
  5. 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.