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

# Adopt Existing Resources in Terraform

> Bring servers, policies and other objects made in the app under Terraform, without recreating them, and reach an empty plan.

Servers, policies and other objects you made in the app or the API can move to Terraform without being recreated. Terraform imports each object, writes its configuration for you, and from then on manages it like any other resource.

This page uses Terraform `import` blocks with `terraform plan -generate-config-out`. Set up the provider first: see [Terraform Provider](https://docs.runlayer.com/terraform-provider.md).

## What you need

* Terraform 1.5 or later (OpenTofu 1.6 or later)
* Runlayer provider 1.14.1 or later. Earlier versions plan changes right after an import: a replace for a catalog server whose `catalog_entry_name` is left out, server settings reset to their defaults, and `runlayer_agent` `owner_id` set to null.
* A user API key, set up as in [Terraform Provider](https://docs.runlayer.com/terraform-provider.md)

```hcl theme={null}
terraform {
  required_providers {
    runlayer = {
      source  = "downloads.runlayer.com/runlayer/runlayer"
      version = "~> 1.14, >= 1.14.1"
    }
  }
}

provider "runlayer" {}
```

## Adopt resources

**1. Find the IDs**

Each import needs the object's import ID. For most resources it is the object's ID; the [import ID table](https://docs.runlayer.com/terraform-provider.md#importing-existing-resources) lists the others.

List servers and policies with the API:

```bash theme={null}
curl -s "$RUNLAYER_BASE_URL/api/v1/servers?limit=500" \
  -H "x-runlayer-api-key: $RUNLAYER_API_KEY" \
  | jq -r '.data[] | "\(.id)  \(.name)"'

curl -s "$RUNLAYER_BASE_URL/api/v1/policies?limit=100" \
  -H "x-runlayer-api-key: $RUNLAYER_API_KEY" \
  | jq -r '.data[] | "\(.id)  \(.description // "")"'
```

One call returns at most 500 servers or 100 policies. If a call returns that many, fetch the next page with `skip`, for example `?skip=500&limit=500`, until a page comes back short.

**2. Write import blocks**

Add one `import` block per object, in a file such as `import.tf`. Do not write the `resource` blocks: Terraform generates them in the next step.

```hcl theme={null}
import {
  to = runlayer_server.internal_api
  id = "8f0c2c3e-5d1a-4b7e-9a51-0c6b2f1d9e42"
}

import {
  to = runlayer_policy.engineers_internal_api
  id = "2b7d4e90-13f6-4c1a-8e0d-6a9f5b3c7d21"
}
```

To adopt every server at once, write the blocks from the API listing:

```bash theme={null}
curl -s "$RUNLAYER_BASE_URL/api/v1/servers?limit=500" \
  -H "x-runlayer-api-key: $RUNLAYER_API_KEY" \
  | jq -r '.data[] | "import {\n  to = runlayer_server.s_\(.name | ascii_downcase | gsub("[^a-z0-9]+"; "_"))\n  id = \"\(.id)\"\n}\n"' \
  > import.tf
```

Two servers whose names differ only in punctuation or case get the same address; rename one before you plan. With more than 500 servers, run it once per page and append (`>>`) the output.

> [!NOTE]
>
> Config generation does not work with `for_each` in `import` blocks. Write one block per object, as above.

**3. Generate the configuration**

```bash theme={null}
terraform init
terraform plan -generate-config-out=generated.tf
```

Terraform reads each object and writes a `resource` block for it to `generated.tf`, which must not exist yet. The plan should end with:

```text theme={null}
Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
```

**4. Review the generated configuration**

Treat `generated.tf` as a starting point:

* **Set every secret from a variable.** Terraform cannot write secret values, so it writes `null # sensitive` instead. A secret left as `null` is cleared on apply. On `runlayer_server` this covers `transport_config.headers`, `transport_config.env` and placeholder defaults.
* **Remove settings you do not want to manage.** On `runlayer_server` and most other resources, a setting left out of the configuration keeps the value stored in Runlayer, so removing a line does not change the object.
* **Replace hard-coded IDs with references** to other resources or data sources, such as `runlayer_group`.
* Move the blocks into the files and modules you want.

```hcl theme={null}
variable "internal_api_token" {
  type      = string
  sensitive = true
}

resource "runlayer_server" "internal_api" {
  name = "Internal API"
  url  = "https://mcp.example.com/mcp"
  transport_config = {
    headers = {
      Authorization = "Bearer ${var.internal_api_token}"
    }
  }
}
```

**5. Plan until there are no changes**

```bash theme={null}
terraform plan
```

Edit the configuration until the plan shows only imports:

```text theme={null}
Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
```

A planned change means the configuration differs from what is stored in Runlayer. Fix the configuration, not the object, unless you mean to change it. Stop if the plan shows a replace (`-/+`): applying it deletes the live object and creates a new one.

**6. Apply**

```bash theme={null}
terraform apply
terraform plan
```

The apply imports the objects without changing them. The plan after it shows `No changes.` You can now delete `import.tf`; import blocks for objects already in state do nothing.

## Things to know

* **`disable_auto_auth` is not stored**, so it cannot be read back, and generated configuration leaves it out. Leaving it out keeps the server's `auth_type`. Add `disable_auto_auth = true` only if later applies must keep `auth_type` as written; the first apply after adding it shows a one-line update.
* **Policy scope wildcards.** When a policy's `scope.tools` or `scope.resources` is all (`*`), generated configuration leaves it out, and that block imports and plans clean. To create a new policy from such a block, set `tools` and `resources` explicitly.
* **Resources without import.** `runlayer_team_member` and `runlayer_team_group_binding` adopt an existing membership or binding when you apply them. Singletons such as `runlayer_organization` write their settings on apply, so declare them with the values you want. See [Importing existing resources](https://docs.runlayer.com/terraform-provider.md#importing-existing-resources).
* **Users, groups and roles** are not resources; read them with the `runlayer_user`, `runlayer_group` and `runlayer_role` data sources, or write their IDs to a tfvars file with `uvx runlayer terraform export`. See [How the CLI can help](https://docs.runlayer.com/terraform-provider.md#how-the-cli-can-help).
* **Server secrets that Runlayer never returns**, such as `manual_oauth_client_secret` and `broker_vendor_client_secret`, are absent after import and from generated configuration. Leaving them out plans no change.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.