Fixing Terraform 'Invalid combination of arguments': Resolving Mutually Exclusive Attributes

beginner๐Ÿ—๏ธ Terraform2026-07-15| Terraform CLI v1.x.x, AWS Provider 4.x/5.x, AzureRM, Google Cloud

Error Message

Error: Invalid combination of arguments An argument named "X" cannot be specified when "Y" is specified.
#terraform#hcl#devops#aws#infrastructure-as-code

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 null to 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'.

Related Error Notes