Which solution do you recommend to handle this unavoidable stateshift?

For okta apps that scim you can’t enable scim through code. you have to apply, enable SCIM, schema will then shift state, then you have to re-apply to make the state match. If I could enable scim through code in any way all of this would be avoided but the terraform team can’t do much because it would require and API Endpoint that doesn’t exist.

I have a count/for-loop resource that ultimately is dependent on a data source that is dependent on a resource within the configuration which will cause an error on the first apply.

  1. Seperate modules and manage with terragrunt

We currently do not use terragrunt but I’m not against it in a major way

  1. Use -target function on first apply in some automated fashion (what that would be I’m not sure)

  2. Figure out if the app exists through a data block then use locals to determine count/for-loop resources

  3. create a boolean in the module that defines if it is the first apply or not.

I would prefer option 3 however I’m new to Terraform and I’m not sure if the work around would be too hacked together where terragrunt would be the way.

The challenge with step 3 is if i list apps by label there isn’t a great way of confirming it is indeed the app I created

Here is how I have thought about working around this.

A. Within the admin note of the app, specify the github repository. The note is created by terraform and is a parseable JSON. Maybe this could be done through a data block using the github provider? Is it adding too much bloat where it’s not worth it? Maybe a local would be acceptable but what if that folder already exists?

B. Put some other GUID in the admin note. How could this GUID be determined before first apply?

C. Create a local file that could get the id and check if it matches okta_app_saml.saml_app.id the challenge is I am planning on using GitHub Actions and remote state so the file would be removed.

1 Like

Hi @andrew-kemp-dahlberg

The most robust way to set this up with Terraform is going to be multiple configurations. When Terraform requires some infrastructure be created before it can plan the rest of the infrastructure, it’s a good indicator the configurations need to be split.

#3 does usually not work, because you are attempting to declare what should be deployed based on what is already deployed, so the configuration will always want to flip-flop between deploying the new resources which don’t exist, and removing them because they already exist.

You can work around this with -target at times, or input variables, but that creates a workflow more dependent on the configuration itself, rather than just having multiple independent configuration in series.

Before you responded I ended up trying number 3. This works consistently but I realize it’s sort of abusive so I’m curious what your thoughts are.

locals {
  find_app_url =  "https://${var.environment.org_name}.${var.environment.base_url}/api/v1/apps?includeNonDeleted=false&q=${local.saml_label}"

}

data "http" "saml_app_list" {
  url = local.find_app_url
  method = "GET"
  request_headers = {
    Accept = "application/json"
    Authorization = "SSWS ${var.environment.api_token}"
  }
}

locals {
  saml_app_id = try(jsondecode(data.http.saml_app_list.response_body)[0].id, "none")
  base_schema_url =  "https://${var.environment.org_name}.${var.environment.base_url}/api/v1/meta/schemas/apps/${local.saml_app_id}/default"
}


data "http" "schema" {
  
  url = local.base_schema_url
  method = "GET"
  request_headers = {
    Accept = "application/json"
    Authorization = "SSWS ${var.environment.api_token}"
  }

}

data "external" "pre-condition" {
  program = ["bash", "-c", <<-EOT
    echo '{"running": "precondition"}'
  EOT
  ]

  lifecycle {
    # Check SAML app list API response
    precondition {
      condition = data.http.saml_app_list.status_code == 200
      error_message = "API request failed with status code: ${data.http.saml_app_list.status_code}. Error: ${data.http.saml_app_list.response_body}"
    }

    # Check SAML app ID
    precondition {
      condition = local.saml_app_id == "none" || local.saml_app_id == try(okta_app_saml.saml_app.id, "n/a")
      error_message = "An application with label '${local.saml_label}' already exists in Okta outside of Terraform. Either modify the label in your configuration or delete/rename the existing application in Okta."
    }

    # Check schema API response
    precondition {
      condition = data.http.schema.status_code == 200 || local.saml_app_id == "none"
      error_message = "Schema API request failed with status code: ${data.http.schema.status_code}. Error: ${data.http.schema.response_body}"
    }
  }
}

locals {
  schema_transformation_status = try(jsondecode(data.http.schema.response_body).definitions.base,"Application does not exist" 
    ) != {
    "id": "#base",
    "type": "object",
    "properties": {
      "userName": {
        "title": "Username",
        "type": "string",
        "required": true,
        "scope": "NONE",
        "maxLength": 100,
        "master": {
          "type": "PROFILE_MASTER"
        }
      }
    },
    "required": [
      "userName"
    ]
  } || var.base_schema == [{
      index       = "userName"
      master      = "PROFILE_MASTER"
      pattern     = tostring(null)
      permissions = "READ_ONLY"
      required    = true
      title       = "Username"
      type        = "string"
      user_type   = "default"
    }] ? "transformation complete or no transformation required" : "pre-transformation"
 

  base_schema = local.schema_transformation_status == "pre-transformation" ? [{
    index       = "userName"
    master      = "PROFILE_MASTER"
    pattern     = null
    permissions = "READ_ONLY"
    required    = true
    title       = "Username"
    type        = "string"
    user_type   = "default"
  }] : var.base_schema
}

resource "okta_app_user_base_schema_property" "properties" {
  count = length(local.base_schema)

  app_id      = okta_app_saml.saml_app.id
  index       = local.base_schema[count.index].index
  title       = local.base_schema[count.index].title
  type        = local.base_schema[count.index].type
  master      = local.base_schema[count.index].master
  pattern     = local.base_schema[count.index].pattern
  permissions = local.base_schema[count.index].permissions
  required    = local.base_schema[count.index].required
  user_type   = local.base_schema[count.index].user_type
}

I’m not really familiar enough with the situation to get a full understanding quickly here, but I don’t see anything super alarming. Basically it looks like you’re using the HTTP requests to incorporate the logic of looking for the resources, which get around the semantics associated with a data source that is meant to directly represent that resource.
If that works for this Okta situation, and you test that it’s reliable, I don’t see any major drawbacks :wink:

For your Terraform SCIM challenge, here’s a brief breakdown:

  1. Terragrunt: Great for managing modules and dependencies, especially for first-time applies.
  2. -target flag: A quick fix for first applies, but not ideal long-term.
  3. Data block & Locals: Use data blocks to check if the app exists, then conditionally manage counts with locals.

For admin notes:

  • Option A (GitHub repo) works cleanly, just avoid bloat.
  • Option B (GUID) is manageable but requires careful state handling.
  • Option C (local file) might be tricky with remote state, so consider fetching the app ID directly via a data block instead.