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.
- Seperate modules and manage with terragrunt
We currently do not use terragrunt but I’m not against it in a major way
-
Use -target function on first apply in some automated fashion (what that would be I’m not sure)
-
Figure out if the app exists through a data block then use locals to determine count/for-loop resources
-
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 
For your Terraform SCIM challenge, here’s a brief breakdown:
- Terragrunt: Great for managing modules and dependencies, especially for first-time applies.
-target
flag: A quick fix for first applies, but not ideal long-term.
- 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.