hashicorp / terraform

Terraform enables you to safely and predictably create, change, and improve infrastructure. It is a source-available tool that codifies APIs into declarative configuration files that can be shared amongst team members, treated as code, edited, reviewed, and versioned.
https://www.terraform.io/
Other
42.73k stars 9.55k forks source link

Support for reasoning about references in the plan file #30826

Open kyorav opened 2 years ago

kyorav commented 2 years ago

Current Terraform Version

Terraform v1.1.7
on windows_amd64

Use-cases

I am writing policies to enforce network security and compliance requirements, and I would like to be able to enforce my policies on IaC prior to deployment. My rules often require resolving references between related networking elements. For example, the rule "VPC flow logging should be enabled" requires examining the "vpc_id" field of all Flow-Log elements to make sure that one of them is pointing to the VPC that I am checking. The problem is that since the id of the vpc is a computed property the value for the target of the flow_log does not appear in the "planned_values" section of the plan file. For simple examples I can get the information from the "configuration" section of the plan file, but when the HCL code uses "for_each" things get more complicated. Here is an example using the IBM provider, but the exact same problem exists with the AWS provider and others.

resource "ibm_is_vpc" "my_vpc" {
  for_each = toset( ["one", "two"] )
  name = "my-vpc-${each.value}"
}

resource ibm_is_flow_log my_flow_log {
  for_each = toset( ["one", "two"] )
  name = "my-flow-log"
  target = ibm_is_vpc.my_vpc[each.value].id
  storage_bucket = "my_bucket_name"
}

In this example each vpc has a flow_log pointing to it and I want to be able to write a policy that can identify that, which means I need to identify that the value of the "target" property of ibm_is_flow_log.my_flow_log["one"] is "ibm_is_vpc.my_vpc[\"one\"].id". However the "target" property doesn't show up at all in the "planned_values" section, while the "for_each" construct is not unrolled in the configuration section.

A similar problem will happen if the flow-log element is defined inside a module and the id of a vpc is passed through an input variable. In this case the configuration section will just give me the name of the input variable because computed values are not propegated in the configuration section.

Let me stress that this is not a problem limited to my work or the IBM provider. I have examined open source policy libraries with policies over AWS and other providers, and found similar issues with the usage of for_each. I would also note that this affects a very important class of policies -- any policy that reasons about the relationship between linked resources.

Attempted Solutions

Currently I pre-process the plan file to find references in the "configuration" section and add the "target" field to the resource definition in the "planned_values" section. This works fine when there is no usage of "for_each" or module input/output variables. For the above example without the for_each, I will find the value "ibm_is_vpc.my_vpc.id" under expressions.target.references[0] in the "configuration" section and set it as the target of my_flow_log. My policy can easily extract the name of the vpc from this value.

Proposal

I can think of two possible enhancements that would support writing policies that depend on object references, although I am sure there must be other options.

(1) Add references to the "planned_values" section. That is, propagate terraform id values in the "planned_values" section even though they are technically computed values. They should be set with some discerning syntax so it is clear that these are references and not constant values.

(2) Unwind "for_each" in the configuration section and propagate terraform id values through input and output variables. I will then be able to find references in the configuration section for my policy to use.

References

I found one possibly related issue:

apparentlymart commented 2 years ago

Hi @kyorav! Thanks for reporting this.

The problem statement you made here makes sense to me and I would like to support policies like this.

I think the challenge is in the design of the mechanisms to support it, since today there are two things in direct conflict with the two improvements you suggested:

I don't think either of these is insurmountable but neither is straightforward either.

I think there might be some other information we could add to the JSON output to help with the sort of cross-referencing you want to do, though. For example, Terraform could in principle attempt to use static or dynamic analysis to understand which resources contributed to a particular unknown value even indirectly, and report those despite them not appearing directly in the expressions. References are always statically known and so static analysis should be possible in principle but we'd need to try it to see if it would be too conservative and thus report "too much" to be useful for the sort of policy you want to write.

Another angle here is that in v1.2 we are planning to add features to declare preconditions for resources, which will allow declaring a rule like this directly inside the Terraform configuration in principle. That feature is aimed more at verifying assumptions that are important for correctness rather than for policy -- that is, more in the category of "this wouldn't work" rather than "this would work but violates an organizational policy" -- so I don't know if that would be an appropriate answer here, but might be if you would consider this rule to be a crucial part of the abstraction the module is providing, rather than a rule imposed broadly and externally across all modules using this resource type.

kyorav commented 2 years ago

Unfortunately the resource precondition option will not help here, since policies must be independent of the infrastructure definition. It seems to me the easiest route is to identify references in the parse phase and then modify the plan generation logic so that it retains information about references. But since I am not familiar with the internals of how this logic works I get that it might be more complex than I imagine.

A cleaner solution might be to add a new type of property in the schema to mark properties that are references, and these would get the special treatment. These properties will have both a computed value (i.e. value known only after deployment) and a planned value (i.e. available in the planned_values section).