No Provisioners
Removes all provisioner blocks from resources. Provisioners bypass the declarative model and are a supply chain risk in shared modules.
When to use this rule
Use this when: You use community Terraform modules from public registries and want to ensure no arbitrary code runs during terraform apply. Or your platform team has banned provisioners as a policy.
Do not use this when: Your modules intentionally use provisioners for bootstrapping that cannot be replaced with user_data, cloud-init, or configuration management tools. This is rare in terraform-aws-modules.
Why this rule exists
Terraform provisioners run arbitrary commands during terraform apply. They don't appear in terraform plan output. In a module sourced from a public registry, provisioner blocks are a supply chain risk: local-exec can modify local files, call APIs, or exfiltrate data.
HashiCorp's own documentation says "provisioners are a last resort." Many platform teams ban them entirely. This rule enforces that ban at the module level by removing all provisioner blocks before the module reaches the developer.
Affected resources
| Resource | Service | Why |
|---|---|---|
* | All resources | Any resource that contains provisioner blocks (local-exec, remote-exec, file) |
What this rule does
Removes every provisioner block from every resource in the module source. The resource itself is kept; only the block is deleted.
Before and after
Before (module with provisioners):
resource "aws_instance" "this" {
ami = var.ami_id
instance_type = var.instance_type
provisioner "local-exec" {
command = "echo ${self.private_ip} >> hosts.txt"
}
provisioner "remote-exec" {
inline = ["sudo apt-get update"]
}
}After (with No Provisioners applied):
resource "aws_instance" "this" {
ami = var.ami_id
instance_type = var.instance_type
}The only change is the rule transformation. All existing arguments, outputs, and module behavior remain identical.
Real-world scenario
A community module included a local-exec provisioner that ran curl to an external API during terraform apply. The API was decommissioned, causing every apply to hang for 30 seconds before timing out. The provisioner was not visible in terraform plan output.
The example above is illustrative. To see this rule run for real, open it in the
Rules Playground β it applies provisioner_remove_blocks to a
reviewed upstream module that actually declares the resources this rule targets,
and shows the diff the registry would serve. No account needed.
Known limits
- Removes the
provisionerblock, not the resource holding it. Anull_resourcethat existed only to run provisioners is left behind as an empty resource that still appears in your plan and state. - Removes
provisionerblocks only. Aconnectionblock that configured aremote-execprovisioner stays in the resource, where it now has nothing to configure. - Does not affect
local-execorremote-execin root module code β only in the downloaded module source. - Does not detect or remove provisioner-like behavior implemented through external data sources or Lambda functions.
- Only
.tfand.tofufiles are read. Modules written in JSON syntax (.tf.json,.tofu.json) are left untouched. - Only the module's own directory and its first-level
modules/*submodules are processed. Modules that this module calls by source URL are separate downloads and are handled on their own request.
Compliance framework support
This rule is not a compliance control. It supports these framework objectives as an operational safeguard:
| Framework | Controls | Role |
|---|---|---|
| SOC 2 | CC8.1 | Supports change management by removing unreviewed imperative code from modules |
| NIST 800-53 | CM-3, SI-7 | Supports configuration management and software integrity |
Default configuration
These are the values the rule ships with. They are not settable from a module source URL: ?rules= carries rule names only, so a per-request enable uses exactly the defaults below. Your organization sets them per rule, in its Baseline or in a ruleset, from the Operational Rules page; see Baseline rules and rulesets.
| Parameter | Type | Default | Description |
|---|---|---|---|
resource_types | list(string) | ["*"] | Target resource type patterns |
The rule's definition
The HCL this rule is written in - its parameters, their shipped defaults, and the transformers it runs - is published at provisioner_remove_blocks, alongside every other selectable rule.
How to enable
On a framework host, per request:
Add ?rules=provisioner_remove_blocks to your HTTPS module source, using the id exactly as written.
module "example" {
source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=provisioner_remove_blocks"
}A ?rules= list is either all bare names, which replace your organization's rules for that download, or all - prefixed names, which subtract from them. Mixing the two is refused with a 400 rather than silently treated as a replacement. To add this rule on top of your organization's rules instead, use ?add_rules=provisioner_remove_blocks; a bare ?rules= list cannot be combined with it, because a bare list already replaced the set there was something to add to.
The + add prefix was removed. It reached the registry only when written %2B, and a literal + in a query string decodes to a space everywhere, so ?rules=+provisioner_remove_blocks arrived as a bare name with a leading space β one step from silently replacing your organization's rules. Any +, in either spelling, is now a 400 naming ?add_rules=.
On your organization's host:
Add the rule to the organization's Baseline, which applies to every module served from the bare organization form <alias>.compliance.tf/<namespace>/<name>/<provider>, or to a named ruleset that one module selects with ?ruleset=<name> on the HTTPS form of that address. A project environment (<alias>.compliance.tf/<project>/<env>/<module>) serves the rules frozen in its bound snapshot instead: there the rule goes into the config draft and reaches the environment by promotion. On both organization forms ?rules= and ?add_rules= are refused with a 400 β including the subtractive - prefix β rather than dropped, so you cannot mistake an ignored override for an applied one. Change the configuration, not the URL. See Baseline rules and rulesets for the configuration flow, and registry resolution for how the address forms differ.
Failure modes
| Scenario | Result |
|---|---|
| Module relies on a provisioner for required setup | The module may not function correctly after provisioner removal. This is rare in terraform-aws-modules. Test in non-production first. |
A null_resource exists only to run a provisioner | The provisioner is removed; the null_resource is not. It survives as an empty resource in your plan and state. Removing the resource itself needs a different transformer, which this rule does not use. |
Terraform and OpenTofu compatible
This rule works with both Terraform (1.x+) and OpenTofu (1.6+). It reads ordinary HCL, and anything it changes stays ordinary HCL, so the module behaves the same under either tool.
Help us improve this page
Operational Rules are a new feature. We'd love your feedback on this rule page β what's useful, what's missing, what's confusing. Share feedback.