Optional Resource fields are not optional?

Am I right that managing only a subset of fields from Resource is not possible in Terraform? The optional fields are still being tracked even when not specified in configuration. If Terraform detects that this optional field is changed, it will try to bring it back to remembered value from the last read.

This means that for any change to “optional” field on remote side, I need to add that field to configuration. Meaning that field is not optional anymore. I can not just “import” values of optional field into state. I can not tell Terraform to notify me about changes in the field and just update the state.


When I omit optional field, such as description for GitLab project, Terraform remembers the value, and if remote side changes it, it tried to change it back.

resource "gitlab_project" "default" {
  name             = "terraform-empty-project"
  visibility_level = "public"
}

I edited the project description from GitLab website, and Terraform tries to reset it to null.

...
      - description                                      = "Update description" -> null
        http_url_to_repo                                 = "https://gitlab.com/abitrolly/terraform-empty-project.git"
...

Am I right that there is no is no way to instruct Terraform to just use the remote value if the description is not set in the config? Use it once, or just don’t pay attention.

Hi @abitrolly,

Generally speaking, Terraform either entirely manages a particular object or does not write to it at all. When a resource type defines an argument as being “optional”, that just means that there is a default value which the provider can use if you don’t set it. In the case of gitlab_project's description, it seems like the default value here is literally null, which presumably means to have no description at all. (I’m not familiar with the GitLab provider or the GitLab API, so I’m just guessing.)

In the unusual case where there’s an argument you want Terraform to ignore after it’s been initially set, you can use the ignore_changes lifecycle override to request that. There are some caveats to it for complex-typed values, but for a simple string like this description argument I think it should achieve the result you are expecting:

resource "gitlab_project" "default" {
  name             = "terraform-empty-project"
  visibility_level = "public"

  lifecycle {
    ignore_changes = [description]
  }
}

There is a consequence to using this, though: the intent of Terraform is that in principle you could run terraform destroy followed by terraform apply and end up with something functionally-equivalent to what you started with (from an object configuration standpoint), but by setting ignore_changes you create a specific situation where that isn’t true: recreating this object would cause any edits of description to be lost.

For this particular object, I expect that consequence is not very important: replacing a GitLab project presumably has much more severe consequences than losing the description, because a GitLab project is a stateful object which has data stored in it that Terraform is not responsible for. I only draw attention to it for completeness, and to explain why ignore_changes is not Terraform’s default behavior.

1 Like

Is it possible to ignore_changes but only if .tf config doesn’t specify own value? I guess in both cases Terraform will refresh the value in the state from remote side, but just will not update the remote with own value.

Hi @abitrolly,

In practice there isn’t really a sense of a value not being set at all. As I mentioned before, omitting an optional argument just means that the provider chooses a default value for you, not that the argument is totally unset. The provider might choose null as the default value and might then decide that null means to omit the value when making the resulting API request, but that’s a provider implementation detail and not something represented in the Terraform language itself.

Consequently, there isn’t any way for ignore_changes to behave differently depending on whether or not you set the value, because from the perspective of the Terraform language the value is always set, just sometimes set by the provider rather than by the configuration. In both cases, ignore_changes causes Terraform to ignore what’s written in the configuration (and writing nothing is still a specific statement that you want to use the default) after the object is first created, always retaining what’s saved in the state.

With GitLab provider complete project resource is set in two steps. After the project is created, the user then needs to commit to repository, for default_branch field to be valid. Therefore the setting is set usually on the second apply, which complicates the understanding of the workflow.

Hi @abitrolly,

In the current Terraform model, that would require either the gitlab_project resource type to encapsulate all three of those steps (create repository, create the branch, set default branch) or to split this into multiple resource types, where each step is its own resource.

For example, it could in principle have a gitlab_project_default_branch resource type whose job is to set the default branch of an existing project, which may or may not be managed by Terraform:

resource "gitlab_project_default_branch" "example" {
  project_id  = gitlab_project.default.id
  branch_name = "main"
}

This sort of design is not very common, but there is precedent for it. For example, aws_default_vpc allows changing settings of an AWS default VPC even though that object is always created by AWS itself, not by Terraform.

With that said, I imagine that the step of pushing a branch isn’t something you’d generally want to do with Terraform – it presumably needs to be done by someone with access to the local git repository the user is importing – so this isn’t a full solution to the problem. I don’t know what to suggest for this; it seems like a situation where Terraform may not be the best tool to solve this particular problem, and perhaps it’s better for someone to use Terraform only to create the project and then to do the rest of the steps separately, in a context that has access to whatever local git repository is going to be pushed up to the new remote repository.