The Frustration of Mutually Exclusive Arguments
I hit a wall last week while refactoring legacy S3 buckets for a client. I was trying to move away from simple canned ACLs toward a more granular ownership control policy. After adding the new configuration block and hitting terraform plan, the terminal fired back with this frustrating error:
Error: Invalid combination of arguments
An argument named "acl" cannot be specified when "access_control_policy" is specified.
Terraform providers enforce strict schemas. When two attributes are marked as mutually exclusive, you have to pick a side. You cannot use both at once. This isn't just a Terraform quirk; it usually reflects the underlying Cloud API's inability to process conflicting instructions in a single request.
Identifying the Conflict
Your first move is to pinpoint exactly which two arguments are clashing. In my AWS example, the conflict lived within the aws_s3_bucket_acl resource. The provider sees the simple acl string and the complex access_control_policy block as two different ways to do the same job.
Here is what the broken code looked like:
resource "aws_s3_bucket_acl" "example" {
bucket = aws_s3_bucket.my_bucket.id
# Legacy attribute
acl = "private"
# New granular policy
access_control_policy {
grant {
grantee {
type = "Group"
uri = "http://acs.amazonaws.com/groups/global/AllUsers"
}
permission = "READ"
}
owner {
id = data.aws_canonical_user_id.current.id
}
}
}
The provider won't guess your intentions. Even if the logic doesn't overlap, the schema only allows one top-level argument for defining permissions. If you leave both, the plan will fail every time.
Refactoring for Clarity
Fixing this requires a hard choice: which attribute represents your "source of truth"? If you are upgrading to a more complex setup, you need to delete the simpler attribute entirely rather than just leaving it blank.
Step 1: Choose Your Logic
Decide which attribute fits your current requirement. For fine-grained control, keep the complex block. If you just need a quick setup, stick to the simple string. Most modern AWS deployments are moving toward access_control_policy for better security auditing.
Step 2: Clean Up the Resource
Here is the corrected version. Notice how the acl line is gone, leaving no room for ambiguity.
resource "aws_s3_bucket_acl" "example" {
bucket = aws_s3_bucket.my_bucket.id
access_control_policy {
grant {
grantee {
type = "CanonicalUser"
id = data.aws_canonical_user_id.current.id
}
permission = "FULL_CONTROL"
}
owner {
id = data.aws_canonical_user_id.current.id
}
}
}
Handling Dynamic Blocks and Conditionals
This error often creeps in when using dynamic blocks or variables to toggle features. If your logic isn't tight, you might accidentally enable both arguments. To prevent this, use the null value to explicitly disable the unused argument.
resource "some_resource" "example" {
# If var.use_simple is true, set attribute_x. Otherwise, set it to null.
attribute_x = var.use_simple ? var.x_value : null
dynamic "attribute_y" {
# Only create the block if var.use_simple is false
for_each = var.use_simple ? [] : [1]
content {
# complex config here
}
}
}
Better Workflow Habits
Iโve started running terraform validate as a standard git pre-commit hook. It takes less than 3 seconds and catches these schema violations locally. This saves you from waiting for a full plan cycle just to find a syntax conflict.
When drafting complex JSON policies, I usually visualize the hierarchy in a YAML โ JSON Converter first. It is much easier to spot structural overlaps in YAML. Once the logic is sound, I convert it to JSON for use in jsonencode() functions. This workflow keeps my HCL files clean and prevents the "argument soup" that leads to these errors.
How to Confirm the Fix
Once you've stripped out the conflicting code, follow these three steps to verify your work:
- Run Validation: Execute
terraform validate. If the error is gone, your syntax matches the provider's schema. - Review the Plan: Run
terraform plan. Look for "forces replacement" warnings. Switching between mutually exclusive arguments sometimes triggers a resource recreation. - Apply the Change: Run
terraform apply. If the cloud API accepts the payload, you're in the clear.
Takeaways
- Check the Docs: Most providers have a "Note" section in their documentation. It will explicitly state if "Argument X is mutually exclusive with Argument Y."
- Explicit Nulls: When using variables, use
nullto ensure an argument is completely ignored by the provider. - Watch for Replacements: Swapping these arguments can be a destructive action. Always check if Terraform intends to destroy and recreate your resource before you hit 'yes'.

