Azure / terraform-azurerm-caf-enterprise-scale

Azure landing zones Terraform module
https://aka.ms/alz/tf
MIT License
798 stars 517 forks source link

Custom variable substitution in the archetype_extension_org.json, possible? #104

Closed calexandre closed 2 years ago

calexandre commented 3 years ago

Community Note

Description

Is it possible to add some kind of variable substitution on archetype_extension_org.json? We have a use-case where we are want to specify the workspace-id for the Deploy-Nsg-FlowLogs-to-LA policy, and unless there's a way to pass that variable from Terraform to the Policy, we are pretty much stuck with the hardcoded value...

Basically, I'm looking to know if its possible to use something in the form of:

        "Deploy-Nsg-FlowLogs-to-LA": {
          "workspace": "${workspace_id}"
        }

Our archetype_extension_org.json with the Deploy-Nsg-FlowLogs-to-LA with a "hardcoded" workspace id...

{
  "extend_es_root": {
    "policy_assignments": [
      "ISO-27001-2013",
      "Deny-PublicEndpoints",
      "Deny-Storage-minTLS",
      "Deny-Resource-Locations",
      "Deny-RSG-Locations"
    ],
    "policy_definitions": [
      "Deny-SSH-From-Internet"
    ],
    "policy_set_definitions": [],
    "role_definitions": [],
    "archetype_config": {
      "parameters": {
        "Deny-Resource-Locations": {
          "listOfAllowedLocations": [
            "westeurope",
            "northeurope"
          ]
        },
        "Deny-RSG-Locations": {
          "listOfAllowedLocations": [
            "westeurope",
            "northeurope"
          ]
        },
        "Deploy-Nsg-FlowLogs-to-LA": {
          "workspace": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
        }
      },
      "access_control": {}
    }
  }
}
krowlandson commented 3 years ago

Absolutely... not tested this exact scenario but the functionality should be available on all files in the custom library_path as we universally implemented the templatefile() function to import these.

The built-in template file variables are based on the following map object:

  builtin_template_file_variables = {
    root_scope_id             = basename(local.root_id)
    root_scope_resource_id    = local.root_id
    current_scope_id          = basename(local.scope_id)
    current_scope_resource_id = local.scope_id
    default_location          = local.default_location
    location                  = local.default_location
    builtin                   = local.builtin_library_path
    builtin_library_path      = local.builtin_library_path
    custom                    = local.custom_library_path
    custom_library_path       = local.custom_library_path
  }

And this can be extended by setting the template_file_variables input variable against your module block, with a custom map(string) object value. As long as the key matches the variable used in the template, the value should be inserted at import, allowing you to perform your custom substitution.

NOTE: We have intentionally ordered the merge to ensure the builtin_template_file_variables value is applied in preference over any conflicting values provided in template_file_variables to prevent unexpected module behaviour, so please ensure you use unique template variable names.

Hope this helps, but feel free to ask if you need further clarification.

krowlandson commented 3 years ago

In a related note (as I'm guessing you may wish to over-ride these values for the built-in policies), we are currently working with @TobiasKeitsch to update the module behaviour so customer-specified parameters take precedence over those managed by the management child module. This will probably help you to more easily set this value across our built-in Policy Assignments too.

You can find more information on Issue #97 and PR #98

krowlandson commented 3 years ago

And one more point. As only string values are supported in the vars input for templatefile(), if you want to enter a more complex type such as array or object into your template, you should be able to use the jsonencode() function to create a correctly formatted JSON string representation of your substitution. It's just worth noting that this may cause problems with some linting tools if you're running code quality checks in your pipeline(s).

calexandre commented 3 years ago

@kevdhowell thank you! We will try your suggestions today and I'll report back the results :) Is there any documentation space prepared for this particular use-case? I could submit a documentation PR if you need.

krowlandson commented 3 years ago

I could submit a documentation PR if you need.

If you could @calexandre, that would be awesome! I'll leave this issue open as a placeholder for tracking. Thank you!

calexandre commented 3 years ago

I tested your suggestion, and I am able to use the template_file_variables. However, I was expecting Terraform to detect some changes based on the value of my change...

MY goal is to replace the hardcored workspace like I mentioned above, and ended with something like this:

archetype_extension_org.json

"Deploy-Nsg-FlowLogs-to-LA": {
  "workspace": "${log_analytics_workspace_id}"
}

And then in my enterprise-scale.tf where the module is used:

template_file_variables = {
  log_analytics_workspace_id = "xpto"
}

Note that I actually used the xpto value just to check if Terraform would plan a change, which it didn't... Any thoughts on this @krowlandson ?

GrundstromT commented 3 years ago

Hi, I looked into your issue and tried to replicate it but it worked as intended for me, you can see my test further down.

In your initial config you don't have any policy_assignment that matches your parameters key Deploy-Nsg-FlowLogs-to-LA. Is the value hardcoded in the policy_assignment_**.tmpl.json file and maybe the parameters from the archetype_extension_**.tmpl.json is ignored?

One test to verify is to set null value for the parameter in the policy_assignment_**.tmpl.json file to verify that the parameter is used. like this:

    "parameters": {
      "workspace": {
        "value": null
      }
    },

Error replication

I started with the following configuration

archetype_extension_es_root.tmpl.json:

{
    "extend_es_root": {
        "policy_assignments": [
            "Deploy-NSG-Flowlog-to-LA"
        ],
        "policy_definitions": [],
        "policy_set_definitions": [],
        "role_definitions": [],
        "archetype_config": {
            "parameters": {
                "Deploy-NSG-Flowlog-to-LA": {
                    "workspace" : "/subscriptions/<guid>/resourcegroups/issue104/providers/microsoft.operationalinsights/workspaces/la-issue104"
                }
            },
            "access_control": {}
        }
    }
}

Then I tried to changed to use the template_file_variables. This did not trigger any changes.

Files: archetype_extension_es_root.tmpl.json:

{
    "extend_es_root": {
        "policy_assignments": [
            "Deploy-NSG-Flowlog-to-LA"
        ],
        "policy_definitions": [],
        "policy_set_definitions": [],
        "role_definitions": [],
        "archetype_config": {
            "parameters": {
                "Deploy-NSG-Flowlog-to-LA": {
                    "workspace" : "${log_analytics_workspace_id}"
                }
            },
            "access_control": {}
        }
    }
}

es_issue104.tf:

module "enterprise_scale" {
  source  = "Azure/caf-enterprise-scale/azurerm"
  version = "0.3.1"

  root_parent_id   = data.azurerm_client_config.current.tenant_id
  default_location = "westeurope"
  root_id          = "es-i104"
  root_name        = "es-i104"
  library_path     = "${path.root}/lib"

  template_file_variables = {
    log_analytics_workspace_id = "/subscriptions/<guid>/resourcegroups/issue104/providers/microsoft.operationalinsights/workspaces/la-issue104"
  }
}

Finally I changed log_analytics_workspace_id in template_file_variables to a mockup value of xpto just as you tried. This triggered a change as it is supposed to:

Terraform will perform the following actions:

  # module.enterprise_scale.azurerm_policy_assignment.enterprise_scale["/providers/Microsoft.Management/managementGroups/es-i104/providers/Microsoft.Authorization/policyAssignments/Deploy-NSG-Flowlog-to-LA"] must be replaced
-/+ resource "azurerm_policy_assignment" "enterprise_scale" {
      ~ id                   = "/providers/Microsoft.Management/managementGroups/es-i104/providers/Microsoft.Authorization/policyAssignments/Deploy-NSG-Flowlog-to-LA" -> (known after apply)
      ~ metadata             = jsonencode(
            {
              - createdBy = "<redacted>"
              - createdOn = "2021-05-17T20:32:47.5535361Z"
              - updatedBy = null
              - updatedOn = null
            }
        ) -> (known after apply)
        name                 = "Deploy-NSG-Flowlog-to-LA"
      ~ parameters           = jsonencode(
          ~ {
              ~ workspace = {
                  ~ value = "/subscriptions/<guid>/resourcegroups/issue104/providers/microsoft.operationalinsights/workspaces/la-issue104" -> "xpto"
                }
            } # forces replacement
        )
        # (6 unchanged attributes hidden)

      ~ identity {
          ~ principal_id = "<redacted>" -> (known after apply)
          ~ tenant_id    = "<redacted>" -> (known after apply)
            # (1 unchanged attribute hidden)
        }
    }
calexandre commented 3 years ago

Hmm thanks for the feedback @TobiasKeitsch! I will try to execute some more tests tomorrow and report back...

krowlandson commented 3 years ago

Hi @calexandre... just wanted to check in to see how you're progressing with this issue? Were you able to resolve this or do you believe there is still an issue we need to investigate? Thank you

calexandre commented 3 years ago

Hello @krowlandson, sorry for the late response, I've been a little busy in the last week..

I was finally able to give this issue some attention and after trying @TobiasKeitsch approach, and I'm still unable to achieve a successful plan...

Some notes regarding the mentioned approach:

  1. The policy assignment that @TobiasKeitsch uses is different from the one I'm currently using (Deploy-Nsg-FlowLogs-to-LA vs Deploy-NSG-Flowlog-to-LA)
  2. After diving a bit further into the module's policies, I've noticed that Deploy-Nsg-FlowLogs-to-LA is a policy definition and not an assignment... I guess this makes a huge difference?
  3. After adding Deploy-Nsg-FlowLogs-to-LA to my archetype's policy_definitions section, the plan executes Ok, but still without any changes... :(
  4. I also tried to add Deploy-Nsg-FlowLogs-to-LA to the archetype's policy_assignments section, which of course ended with an error because it is a definition and not an assignment:
╷
│ Error: Invalid index
│ 
│   on .terraform/modules/enterprise_scale/modules/archetypes/locals.policy_assignments.tf line 92, in locals:
│   92:       template    = local.archetype_policy_assignments_map[policy_assignment]
│     ├────────────────
│     │ local.archetype_policy_assignments_map is object with 30 attributes
│ 
│ The given key does not identify an element in this collection value.

Since this is a built-in policy definition, should I be trying to pass a value to the definition, or should I create a new policy assignment instead?

GrundstromT commented 3 years ago

You need to create a policy assignment, I used the same name as the definition unfortunately.

In the archetype you need to define both the definition and the assignment, and in the parameter object you should map it to the name of the policy assignment that you create.

calexandre commented 3 years ago

That explains a lot :)

Could you share the policy assignment you created? Could use a working example :)

krowlandson commented 3 years ago

You can always reference the Policy Assignment templates (and others) which are bundled with the module.

For example, the Deny-Resource-Locations Policy Assignment is an example of one which references a "built-in" Policy Definition, whilst Deny-RDP-From-Internet references a "custom" Policy Definition provisioned by the module. This let's you see how we use the default template variables to customise these during import, which will behave in the same way with values provided via the template_file_variables input variable.

krowlandson commented 3 years ago

Also worth noting that you do not have to provide parameters within the template (just a placeholder for the parameters block), unless you want to hard code values into the assignment.

In practice, we would recommend settings these at the scope of assignment using the parameters object within archetype_config.

calexandre commented 3 years ago

Thank you for the explanation! :) I will try again today using your recommendations and report back 👍

krowlandson commented 2 years ago

Once we cut the next release, this issue should be addressed by the updates to the [Variables] template_file_variables page on our Wiki.