Until now, catching up was expensive: one terraform import command per resource, an HCL block written by hand for each one, attribute by attribute, and a plan that offers to destroy everything at the slightest missing line. Forty resources, a day gone. Clever Tools 5.3 and version 3.0 of our Terraform provider bring the operation down to three commands: clever features, clever terraform generate and terraform plan. Here is how they fit together and, more importantly, what to check before you apply.
What you need
You need three tools: Terraform 1.5 or later (or OpenTofu 1.6 or later, the Terraform fork, which implements the same import blocks), Clever Tools 5.3 and version 3.0 of the Clever Cloud provider. The command we need is still in beta, so you have to enable it explicitly:
npm install -g clever-tools@latest
clever login
clever features enable terraform
A word about our provider
Open source from day one and published on both the Terraform and OpenTofu registries, the provider now covers most of the platform: runtimes, managed databases and add-ons, object storage and FS Buckets, log drains, Network Groups, Kubernetes clusters and even OAuth consumers. Beyond provisioning, it can deploy your code from a Git repository and pin a commit, link an application to its add-ons the way clever service link-addon does, read the load balancer address for your DNS records, or browse a database’s backups. Since version 3.0, it accepts an API token for CI integration, and with Terraform 1.14 and later, its actions restart an application, run a SQL query against PostgreSQL or upload files to an FS Bucket without leaving terraform apply. Forty-eight resource types, two data sources and three actions: enough to describe an entire organisation, not just import it.
Step 1: declare the Clever Cloud Terraform provider
In an empty directory, create a provider.tf file:
terraform {
required_providers {
clevercloud = {
source = "CleverCloud/clevercloud"
version = "~> 3.0"
}
}
}
provider "clevercloud" {}
The provider block stays empty: after clever login, the provider reuses the credentials of the active Clever Tools profile. All it still needs is the organisation to target, passed through an environment variable (a user_... identifier also works for a personal space):
export CC_ORGANISATION="orga_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
terraform init
Step 2: generate import blocks with clever terraform generate
This is where the new feature in Clever Tools 5.3 comes in. The clever terraform generate command goes through every application and add-on in an organisation and writes the import block Terraform expects for each one:
clever terraform generate --org orga_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx > imports.tf
The resulting file looks like this:
# my-api
import {
to = clevercloud_nodejs.my-api
id = "app_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
# my-db
import {
to = clevercloud_postgresql.my-db
id = "postgresql_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
Each application is mapped to the resource type for its runtime (clevercloud_nodejs, clevercloud_php, clevercloud_docker, clevercloud_java_maven…), and each add-on to its own (clevercloud_postgresql, clevercloud_redis, clevercloud_cellar…).
The command also filters out what you should not import. A Keycloak, Matomo, Metabase or Otoroshi add-on provisions its own application and databases, and an Elasticsearch add-on comes with its own Kibana. These resources show up in your organisation like any other, but they belong to the add-on that manages them. Importing them would hand them over to Terraform, which would then start changing them alongside the add-on. clever terraform generate skips them and reports it on standard error, just as it reports the few runtimes that have no equivalent in the provider yet. Nothing is dropped silently.
The names in the Terraform addresses are derived from your application and add-on names: lowercased, stripped of special characters, and prefixed with _ if they start with a digit. Two resources with the same type and name, two Node.js applications both called “demo”, for example, cannot coexist in one configuration. The second one gets a suffix taken from its identifier (demo-11ced66e), and standard error reports it (Name collision on nodejs.demo: suffixing with 11ced66e). These names are only labels in your code: rename them as you like in imports.tf before step 3, and Terraform will generate the configuration under the new name.
To adopt Terraform gradually, you can restrict generation to a single application, or to resources with a given tag:
clever terraform generate --app my-api > imports.tf
clever terraform generate --tag env:staging > imports.tf
Step 3: generate the HCL configuration with terraform plan
Since version 1.5, Terraform can write the configuration for the resources targeted by import blocks on its own:
terraform plan -generate-config-out=generated.tf
Terraform reads each resource from the platform, writes its HCL block to generated.tf (the file must not already exist) and displays the corresponding plan, without writing anything to the state. The file contains the actual values: name, region, minimum and maximum instance counts, flavors, the add-on’s plan and dependencies.
One point needs explaining. On an application, min_instance_count, max_instance_count, smallest_flavor and biggest_flavor are required, as is plan on an add-on, and they have no default: the provider will never fill them in on its own. These attributes determine what the resource costs, and we refuse to let a configuration stay silent about them and quietly adopt whatever the platform happens to be running. This is also the reason to prefer import blocks over the terraform import command, which writes directly to the state and requires you to write the full resource block yourself beforehand, or face a Missing required argument error on the next plan.
Secrets and environment variables: the Terraform import trap
This is the step where you need to slow down. Terraform never copies a value that the provider declares sensitive into a generated configuration, and the provider declares an application’s environment variables sensitive. For each application, the generated file therefore contains:
resource "clevercloud_nodejs" "my-api" {
name = "my-api"
region = "par"
min_instance_count = 1
max_instance_count = 2
smallest_flavor = "XS"
biggest_flavor = "M"
environment = null # sensitive
}
Applying this file as is would delete all of the application’s environment variables. The provider offers an environment variable that works around this behaviour while you generate the configuration:
BYPASS_SENSITIVE_IMPORTS=true terraform plan -generate-config-out=generated.tf
The provider then stops reporting these attributes as sensitive, so Terraform writes them out like any other, and environment contains all your variables in plain text. The file is not the only place: as long as the variable is set, the output of terraform plan also shows these values in plain text. Run this step from your own machine, never in a CI pipeline whose logs are kept. That is exactly what we wanted, and it is also why you need to be careful: API keys, external database passwords, third-party service tokens, everything you stored with clever env set ends up in a .tf file that you are about to commit.
Before the first commit, review these variables. Ordinary configuration values (NODE_ENV, a port, a public URL) can stay in the code. Secrets have to move out, to whatever tool your team already uses: HashiCorp Vault or OpenBao, your CI’s protected variables, a file encrypted with SOPS, or at the very least Terraform variables marked sensitive and supplied at run time:
variable "stripe_api_key" {
type = string
sensitive = true
}
resource "clevercloud_nodejs" "my-api" {
# ...
environment = {
NODE_ENV = "production"
STRIPE_API_KEY = var.stripe_api_key
}
}
This change shows up on the next plan, as an in-place update to environment even though the value has not changed: Terraform only records in the state that this value is now sensitive. The provider compares values, not their status, so identical values do not trigger an application restart.
Remove BYPASS_SENSITIVE_IMPORTS as soon as generated.tf is written. The configuration remains valid without it, and the provider displays the warning Sensitive attributes are not redacted on every command for as long as the variable is set. As for the Terraform state, it always contains these values, whatever you choose, so storing it deserves the same care as the secrets themselves.
Step 4: check the plan, then apply
Once you have reviewed and cleaned up generated.tf, running terraform plan without the bypass variable should show one import per resource, and no change other than the environment attributes whose secrets you have just marked as sensitive:
terraform plan
# Plan: 2 to import, 0 to add, 1 to change, 0 to destroy.
# ~ environment = (sensitive value) value unchanged, sensitive status recorded
terraform apply
Any other change means that an attribute in the configuration differs from what the platform is running: read it before confirming.
The apply then records the resources in the state, along with the sensitivity of the secrets passed as variables. It does not touch anything on the platform. When a final terraform plan reports No changes., your infrastructure is described in code and you can remove the import blocks.
Code deployment stays your call
An import never fills in an application’s deployment block. The provider cannot guess whether you want Terraform to manage your deployments as well, and adding this block triggers a deployment on the first apply.
You have three options. The first is to add nothing: Terraform manages the configuration, you keep deploying with git push or clever deploy, and a change to an environment variable restarts the application, just as it does today. The second is to set commit = "github_hook" in the deployment block if the application is linked to a GitHub repository: deployments remain driven by GitHub, and the provider never pushes code. The third is to hand deployment over to Terraform with repository and, if you wish, a specific commit. In that case, an API token (clever tokens create) is not enough, because it cannot push to Git, so the provider needs the OAuth credentials of the Clever Tools profile.
After the import
The generated.tf file is a starting point, not a finished configuration. A few changes make it maintainable: split the file by service or by environment, replace the hard-coded identifiers in the dependencies attribute with references (clevercloud_postgresql.my-db.id), and move whatever differs between staging and production into variables. And since the command is in beta, your feedback is welcome on the Clever Tools GitHub repository: that is how it will move out of beta.
That leaves the question of the state, which you currently keep in an S3 bucket or a third-party backend. We will not say more for now, except that it may soon no longer need to leave Clever Cloud.
FAQ
Does the import modify my applications?
No. Import blocks only write to the state. The only expected update is to environment, when you have replaced a secret with a sensitive variable: the value is identical and the provider does not restart the application. The only real risk is applying a configuration in which environment is null, which is the whole point of the section on secrets.
Can I import only part of my organisation?
Yes, with --app for a single application or --tag for resources with a given tag. Nothing forces you to migrate everything at once.
Why are some resources missing from the imports.tf file?
Either they are managed by an add-on (a Keycloak add-on’s application, an Elasticsearch add-on’s Kibana) and Terraform must not take them over, or their runtime does not have a resource in the provider yet. In both cases, the command says so on standard error.
Does it work with OpenTofu, the Terraform fork?
Yes. OpenTofu has supported import blocks and the -generate-config-out flag since version 1.6, and our provider is available on its registry.
Should I keep the import blocks?
No. Once the resources are in the state, they no longer serve any purpose and can be deleted.
Clever Tools 5.3 is open source on GitHub, available on npm and in our package repositories. The Clever Cloud provider is published on the Terraform Registry, its code is on GitHub, and the full documentation is available at www.clever.cloud/developers.
That is exactly when infrastructure as code (IaC, the practice of defining your resources in versioned files that act as the source of truth) becomes the obvious next step, and when you discover that Terraform knows nothing about what already exists.
Until now, catching up was expensive: one terraform import command per resource, an HCL block written by hand for each one, attribute by attribute, and a plan that offers to destroy everything at the slightest missing line. Forty resources, a day gone. Clever Tools 5.3 and version 3.0 of our Terraform provider bring the operation down to three commands: clever features, clever terraform generate and terraform plan. Here is how they fit together and, more importantly, what to check before you apply.
What you need
You need three tools: Terraform 1.5 or later (or OpenTofu 1.6 or later, the Terraform fork, which implements the same import blocks), Clever Tools 5.3 and version 3.0 of the Clever Cloud provider. The command we need is still in beta, so you have to enable it explicitly:
npm install -g clever-tools@latest
clever login
clever features enable terraform
A word about our provider
Open source from day one and published on both the Terraform and OpenTofu registries, the provider now covers most of the platform: runtimes, managed databases and add-ons, object storage and FS Buckets, log drains, Network Groups, Kubernetes clusters and even OAuth consumers. Beyond provisioning, it can deploy your code from a Git repository and pin a commit, link an application to its add-ons the way clever service link-addon does, read the load balancer address for your DNS records, or browse a database’s backups. Since version 3.0, it accepts an API token for CI integration, and with Terraform 1.14 and later, its actions restart an application, run a SQL query against PostgreSQL or upload files to an FS Bucket without leaving terraform apply. Forty-eight resource types, two data sources and three actions: enough to describe an entire organisation, not just import it.
Step 1: declare the Clever Cloud Terraform provider
In an empty directory, create a provider.tf file:
terraform {
required_providers {
clevercloud = {
source = "CleverCloud/clevercloud"
version = "~> 3.0"
}
}
}
provider "clevercloud" {}
The provider block stays empty: after clever login, the provider reuses the credentials of the active Clever Tools profile. All it still needs is the organisation to target, passed through an environment variable (a user_... identifier also works for a personal space):
export CC_ORGANISATION="orga_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
terraform init
Step 2: generate import blocks with clever terraform generate
This is where the new feature in Clever Tools 5.3 comes in. The clever terraform generate command goes through every application and add-on in an organisation and writes the import block Terraform expects for each one:
clever terraform generate --org orga_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx > imports.tf
The resulting file looks like this:
# my-api
import {
to = clevercloud_nodejs.my-api
id = "app_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
# my-db
import {
to = clevercloud_postgresql.my-db
id = "postgresql_xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
Each application is mapped to the resource type for its runtime (clevercloud_nodejs, clevercloud_php, clevercloud_docker, clevercloud_java_maven…), and each add-on to its own (clevercloud_postgresql, clevercloud_redis, clevercloud_cellar…).
The command also filters out what you should not import. A Keycloak, Matomo, Metabase or Otoroshi add-on provisions its own application and databases, and an Elasticsearch add-on comes with its own Kibana. These resources show up in your organisation like any other, but they belong to the add-on that manages them. Importing them would hand them over to Terraform, which would then start changing them alongside the add-on. clever terraform generate skips them and reports it on standard error, just as it reports the few runtimes that have no equivalent in the provider yet. Nothing is dropped silently.
The names in the Terraform addresses are derived from your application and add-on names: lowercased, stripped of special characters, and prefixed with _ if they start with a digit. Two resources with the same type and name, two Node.js applications both called “demo”, for example, cannot coexist in one configuration. The second one gets a suffix taken from its identifier (demo-11ced66e), and standard error reports it (Name collision on nodejs.demo: suffixing with 11ced66e). These names are only labels in your code: rename them as you like in imports.tf before step 3, and Terraform will generate the configuration under the new name.
To adopt Terraform gradually, you can restrict generation to a single application, or to resources with a given tag:
clever terraform generate --app my-api > imports.tf
clever terraform generate --tag env:staging > imports.tf
Step 3: generate the HCL configuration with terraform plan
Since version 1.5, Terraform can write the configuration for the resources targeted by import blocks on its own:
terraform plan -generate-config-out=generated.tf
Terraform reads each resource from the platform, writes its HCL block to generated.tf (the file must not already exist) and displays the corresponding plan, without writing anything to the state. The file contains the actual values: name, region, minimum and maximum instance counts, flavors, the add-on’s plan and dependencies.
One point needs explaining. On an application, min_instance_count, max_instance_count, smallest_flavor and biggest_flavor are required, as is plan on an add-on, and they have no default: the provider will never fill them in on its own. These attributes determine what the resource costs, and we refuse to let a configuration stay silent about them and quietly adopt whatever the platform happens to be running. This is also the reason to prefer import blocks over the terraform import command, which writes directly to the state and requires you to write the full resource block yourself beforehand, or face a Missing required argument error on the next plan.
Secrets and environment variables: the Terraform import trap
This is the step where you need to slow down. Terraform never copies a value that the provider declares sensitive into a generated configuration, and the provider declares an application’s environment variables sensitive. For each application, the generated file therefore contains:
resource "clevercloud_nodejs" "my-api" {
name = "my-api"
region = "par"
min_instance_count = 1
max_instance_count = 2
smallest_flavor = "XS"
biggest_flavor = "M"
environment = null # sensitive
}
Applying this file as is would delete all of the application’s environment variables. The provider offers an environment variable that works around this behaviour while you generate the configuration:
BYPASS_SENSITIVE_IMPORTS=true terraform plan -generate-config-out=generated.tf
The provider then stops reporting these attributes as sensitive, so Terraform writes them out like any other, and environment contains all your variables in plain text. The file is not the only place: as long as the variable is set, the output of terraform plan also shows these values in plain text. Run this step from your own machine, never in a CI pipeline whose logs are kept. That is exactly what we wanted, and it is also why you need to be careful: API keys, external database passwords, third-party service tokens, everything you stored with clever env set ends up in a .tf file that you are about to commit.
Before the first commit, review these variables. Ordinary configuration values (NODE_ENV, a port, a public URL) can stay in the code. Secrets have to move out, to whatever tool your team already uses: HashiCorp Vault or OpenBao, your CI’s protected variables, a file encrypted with SOPS, or at the very least Terraform variables marked sensitive and supplied at run time:
variable "stripe_api_key" {
type = string
sensitive = true
}
resource "clevercloud_nodejs" "my-api" {
# ...
environment = {
NODE_ENV = "production"
STRIPE_API_KEY = var.stripe_api_key
}
}
This change shows up on the next plan, as an in-place update to environment even though the value has not changed: Terraform only records in the state that this value is now sensitive. The provider compares values, not their status, so identical values do not trigger an application restart.
Remove BYPASS_SENSITIVE_IMPORTS as soon as generated.tf is written. The configuration remains valid without it, and the provider displays the warning Sensitive attributes are not redacted on every command for as long as the variable is set. As for the Terraform state, it always contains these values, whatever you choose, so storing it deserves the same care as the secrets themselves.
Step 4: check the plan, then apply
Once you have reviewed and cleaned up generated.tf, running terraform plan without the bypass variable should show one import per resource, and no change other than the environment attributes whose secrets you have just marked as sensitive:
terraform plan
# Plan: 2 to import, 0 to add, 1 to change, 0 to destroy.
# ~ environment = (sensitive value) value unchanged, sensitive status recorded
terraform apply
Any other change means that an attribute in the configuration differs from what the platform is running: read it before confirming.
The apply then records the resources in the state, along with the sensitivity of the secrets passed as variables. It does not touch anything on the platform. When a final terraform plan reports No changes., your infrastructure is described in code and you can remove the import blocks.
Code deployment stays your call
An import never fills in an application’s deployment block. The provider cannot guess whether you want Terraform to manage your deployments as well, and adding this block triggers a deployment on the first apply.
You have three options. The first is to add nothing: Terraform manages the configuration, you keep deploying with git push or clever deploy, and a change to an environment variable restarts the application, just as it does today. The second is to set commit = "github_hook" in the deployment block if the application is linked to a GitHub repository: deployments remain driven by GitHub, and the provider never pushes code. The third is to hand deployment over to Terraform with repository and, if you wish, a specific commit. In that case, an API token (clever tokens create) is not enough, because it cannot push to Git, so the provider needs the OAuth credentials of the Clever Tools profile.
After the import
The generated.tf file is a starting point, not a finished configuration. A few changes make it maintainable: split the file by service or by environment, replace the hard-coded identifiers in the dependencies attribute with references (clevercloud_postgresql.my-db.id), and move whatever differs between staging and production into variables. And since the command is in beta, your feedback is welcome on the Clever Tools GitHub repository: that is how it will move out of beta.
That leaves the question of the state, which you currently keep in an S3 bucket or a third-party backend. We will not say more for now, except that it may soon no longer need to leave Clever Cloud.
FAQ
Does the import modify my applications?
No. Import blocks only write to the state. The only expected update is to environment, when you have replaced a secret with a sensitive variable: the value is identical and the provider does not restart the application. The only real risk is applying a configuration in which environment is null, which is the whole point of the section on secrets.
Can I import only part of my organisation?
Yes, with --app for a single application or --tag for resources with a given tag. Nothing forces you to migrate everything at once.
Why are some resources missing from the imports.tf file?
Either they are managed by an add-on (a Keycloak add-on’s application, an Elasticsearch add-on’s Kibana) and Terraform must not take them over, or their runtime does not have a resource in the provider yet. In both cases, the command says so on standard error.
Does it work with OpenTofu, the Terraform fork?
Yes. OpenTofu has supported import blocks and the -generate-config-out flag since version 1.6, and our provider is available on its registry.
Should I keep the import blocks?
No. Once the resources are in the state, they no longer serve any purpose and can be deleted.
Clever Tools 5.3 is open source on GitHub, available on npm and in our package repositories. The Clever Cloud provider is published on the Terraform Registry, its code is on GitHub, and the full documentation is available at www.clever.cloud/developers.