hashicorp/terraform · error

failed to unlock both S3 and DynamoDB: S3 error

Error message

failed to unlock both S3 and DynamoDB: S3 error: %v, DynamoDB error: %v

What it means

During dual-lock unlock (both S3 file and DynamoDB enabled), Terraform attempts both release paths independently. This error reports that BOTH the S3 file unlock and the DynamoDB unlock failed, leaving the state fully locked. The two underlying errors are embedded with %v (not %w), so the original lockErr context carries the failure.

Solutions

  1. Refresh AWS credentials (`aws sts get-caller-identity` must succeed) and retry `terraform force-unlock <id>`; transient dual failures often clear on retry.
  2. Manually remove both locks: delete the S3 `.tflock` object AND the DynamoDB LockID row, using the lock ID from the error.
  3. Check the AWS status page for an active S3/DynamoDB incident in the configured region before retrying.
  4. Verify the IAM principal retains s3:GetObject, s3:DeleteObject, and dynamodb:DeleteItem on the relevant resources.
  5. Inspect both underlying %v messages to determine if one failure is benign (e.g. NoSuchKey on an already-deleted lock file) so you can target the real blockage.

Example fix

# manually clear both locks after a dual unlock failure
aws s3api delete-object --bucket tf-state-prod --key prod/terraform.tflock.tflock
aws dynamodb delete-item \
  --table-name terraform-locks \
  --key '{"LockID":{"S":"tf-state-prod/prod/terraform.tfstate"}}'
Defensive patterns

Strategy: retry

Validate before calling

// Confirm both backends are reachable before attempting dual unlock.
func canDualUnlock(ctx context.Context, s3c *s3.Client, ddb *dynamodb.Client, bucket, table string) error {
  if _, err := s3c.HeadBucket(ctx, &s3.HeadBucketInput{Bucket: &bucket}); err != nil {
    return err
  }
  if _, err := ddb.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {
    return err
  }
  return nil
}

Try / catch

// After a dual-unlock failure, instruct the operator with both errors and the lock ID.
if ferr != nil && derr != nil {
  lockErr.Err = fmt.Errorf("failed to unlock both S3 and DynamoDB: S3 error: %v, DynamoDB error: %v; "+
    "run `terraform force-unlock %s` once access is restored", ferr, derr, id)
  return lockErr
}

Prevention

When it happens

Trigger: Both unlockWithFile (client.go:481) and unlockWithDynamoDB (client.go:482) return non-nil errors. Triggers: simultaneous loss of S3 and DynamoDB access (credentials revoked mid-run, region outage), permissions revoked for both services, or the lock file and DDB item were both already deleted by a concurrent force-unlock causing both lookups to fail.

Common situations: AWS regional incident affecting both S3 and DynamoDB, an expired STS session that breaks all AWS calls during a long apply, or two operators force-unlocking concurrently so each unlock path finds the target already gone.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/1df679d7381be333. Report an issue: GitHub.

Appendix: source

Thrown at internal/backend/remote-state/s3/client.go:485

	if !c.useLockFile && c.ddbTable != "" {
		log.Info("Attempting to unlock remote state (DynamoDB only)...")
		if err := c.unlockWithDynamoDB(ctx, id, lockErr); err != nil {
			lockErr.Err = err
			return lockErr
		}

		log.Info("Unlocked remote state (DynamoDB only)")
		return nil
	}

	// Double unlocking: DynamoDB + file
	log.Info("Attempting to unlock remote state (S3 Native and DynamoDB)...")

	ferr := c.unlockWithFile(ctx, id, lockErr, log)
	derr := c.unlockWithDynamoDB(ctx, id, lockErr)

	if ferr != nil && derr != nil {
		lockErr.Err = fmt.Errorf("failed to unlock both S3 and DynamoDB: S3 error: %v, DynamoDB error: %v", ferr, derr)
		return lockErr
	}

	if ferr != nil {
		lockErr.Err = fmt.Errorf("failed to unlock S3: %v", ferr)
		return lockErr
	}

	if derr != nil {
		lockErr.Err = fmt.Errorf("failed to unlock DynamoDB: %v", derr)
		return lockErr
	}

	log.Info("Unlocked remote state (S3 Native and DynamoDB)")
	return nil
}

// unlockWithFile attempts to unlock the remote state by deleting the lock file from Amazon S3.

View on GitHub (pinned to d32a084675)