terraformgitlab-citerraform-provider-gitlab

How to avoid overwriting GitLab CI variables when using Terraform


I am new to Terraform and am trying to provision my GitLab project using it. This works so far, but I am also creating keys that must not be reset with every run. Therefore, the GitLab CI variables should only be created if they do not already exist. Unfortunately, I have no idea how to formulate this in Terraform.

This is the relevant part of my configuration:

variable "env_scopes_all" {
  type = list(object({
    scope    = string
    env_type = string
  }))
  default = [
    { scope = "prod/live", env_type = "live" },
    { scope = "prod/staging", env_type = "staging" },
    { scope = "prod/review-*", env_type = "review" }
  ]
}

data "external" "vapid_keys" {
  count = length(var.env_scopes_all)
  program = ["/bin/sh", "-c", <<-EOF
    pem=$(openssl ecparam -name prime256v1 -genkey -noout)
    private_key=$(echo "$pem" | openssl ec -outform DER 2>/dev/null | tail -c +8 | head -c 32 | base64 -w 0 | tr -d '=' | tr '/+' '_-')
    public_key=$(echo "$pem" | openssl ec -pubout -outform DER 2>/dev/null | tail -c 65 | base64 -w 0 | tr -d '=' | tr '/+' '_-')

    printf '{"private_key":"%s","public_key":"%s"}\n' "$private_key" "$public_key"
  EOF
  ]
}

resource "gitlab_project_variable" "VAPID_PRIVATE_KEY" {
  count             = length(var.env_scopes_all)
  project           = gitlab_project.elybird.id
  key               = "VAPID_PRIVATE_KEY"
  value             = data.external.vapid_keys[count.index].result.private_key
  description       = "Base64-encoded VAPID private key for push notifications"
  environment_scope = var.env_scopes_all[count.index].scope
  masked            = false
  protected         = false
  raw               = true
  variable_type     = "env_var"
}

resource "gitlab_project_variable" "VAPID_PUBLIC_KEY" {
  count             = length(var.env_scopes_all)
  project           = gitlab_project.elybird.id
  key               = "VAPID_PUBLIC_KEY"
  value             = data.external.vapid_keys[count.index].result.public_key
  description       = "Base64-encoded VAPID public key for client push subscription"
  environment_scope = var.env_scopes_all[count.index].scope
  masked            = false
  protected         = false
  raw               = true
  variable_type     = "env_var"
}

Solution

  • After having a discussion in the comments I am going to add this as an answer here around using ignore_changes. I personally still dont feel like this is the best option for you. But thats up to you to decide.

    I am using a simple example Since I dont have gitlab setup to demo this. The example uses a var in the external data source but this should be similar to your example

    variable "custom_length" {
        type = number
        description = "Custom length for the random_pet resource"
    }
    
    data "external" "foo" {
      program = ["powershell", "-Command", "echo '{\"counter\":\"${var.custom_length}\"}'"]
    }
    
    resource "random_pet" "foo" {
      length = data.external.foo.result["counter"]
      lifecycle {ignore_changes = [length]}
    }
    
    output "counter" {
      value = data.external.foo.result["counter"]
    }
    
    output "pet_length" {
      value = random_pet.foo.length
    }
    

    If we run this you can see that on the first apply it creates 1 resource. On the 2nd apply with a different value it does indeed ignore the change in the attribute after its created. It does not create or destroy or change any resources, only the output changes.

    I have added the output only for reference so you can see the data block output is changing but terraform doesnt change the resources because we are ignoring that attributes value.

    First Create

    $ terraform apply --var=custom_length=3 --auto-approve
    data.external.foo: Reading...
    data.external.foo: Read complete after 0s [id=-]
    
    Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
      + create
    
    Terraform will perform the following actions:
    
      # random_pet.foo will be created
      + resource "random_pet" "foo" {
          + id        = (known after apply)
          + length    = 3
          + separator = "-"
        }
    
    Plan: 1 to add, 0 to change, 0 to destroy.
    
    Changes to Outputs:
      + counter    = "3"
      + pet_length = 3
    random_pet.foo: Creating...
    random_pet.foo: Creation complete after 0s [id=needlessly-wanted-hare]
    
    Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
    
    Outputs:
    
    counter = "3"
    pet_length = 3
    

    Second Apply (Changed value)

    $ terraform apply --var=custom_length=5 --auto-approve
    data.external.foo: Reading...
    data.external.foo: Read complete after 0s [id=-]
    random_pet.foo: Refreshing state... [id=needlessly-wanted-hare]
    
    Changes to Outputs:
      ~ counter    = "3" -> "5"
    
    You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
    
    Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
    
    Outputs:
    
    counter = "5"
    pet_length = 3
    

    You can see in the 2nd apply there is no changes in the infra, the outputs show that the external resource is returning 5, but the random_pet keeps its value as 3 and terraform decides there is no change to be made in the resource.