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.
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 |
How to enable
On a framework host, per request:
Add ?rules=provisioner_remove_blocks to your HTTPS module source. Use the full namespaced id. A bare provisioner_remove_blocks matches no rule: it is skipped server-side and your terraform init still succeeds, without the rule.
module "example" {
source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=provisioner_remove_blocks"
}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= is 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.