hashicorp/terraform · error · statemgr.LockError

failed to unlock DynamoDB: %v

Error message

failed to unlock DynamoDB: %v

What it means

Returned by RemoteClient.Unlock in dual-locking mode when the DynamoDB lock-item deletion failed but the S3 file delete succeeded. It wraps the error from unlockWithDynamoDB, which can originate from getLockInfoWithDynamoDB (GetItem) or the final DeleteItem call. The S3 lock is gone, so only the DynamoDB row remains.

Source

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

	// 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.
//
// This method is used when the S3 native locking mechanism is in use, which uses a `.tflock` file
// to manage state locking. The function deletes the lock file to release the lock, allowing other
// Terraform clients to acquire the lock on the same state file.
func (c *RemoteClient) unlockWithFile(ctx context.Context, id string, lockErr *statemgr.LockError, log hclog.Logger) error {
	getInput := &s3.GetObjectInput{
		Bucket: aws.String(c.bucketName),
		Key:    aws.String(c.lockFilePath),
	}

View on GitHub (pinned to c9def3e214)

Solutions

  1. Parse the wrapped DynamoDB error: ResourceNotFoundException means the table is gone, ProvisionedThroughputExceededException needs capacity scaling, AccessDenied needs an IAM fix.
  2. Confirm the configured dynamodb_table still exists and matches the one holding the LockID item.
  3. Grant dynamodb:GetItem + dynamodb:DeleteItem on the table ARN to the running principal.
  4. Delete the stale row directly: aws dynamodb delete-item --table-name <tbl> --key '{"LockID":{"S":"<bucket>/<path>"}}'.

Example fix

// before: table was deleted
// after: recreate or point backend at the correct table
terraform {
  backend "s3" {
    dynamodb_table = "terraform-locks" # must exist
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Confirm the DynamoDB table exists before running
func ensureLockTable(ctx context.Context, ddbs *dynamodb.Client, table string) error {
  if _, err := ddbs.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {
    return fmt.Errorf("lock table %q unavailable: %w", table, err)
  }
  return nil
}

Try / catch

err := client.Unlock(id)
if le, ok := err.(*statemgr.LockError); ok && strings.Contains(le.Err.Error(), "failed to unlock DynamoDB") {
    // only DDB failed; S3 is clean, fix DDB side then retry
}

Prevention

When it happens

Trigger: Unlock(id) with useLockFile=true && ddbTable set, where DynamoDB GetItem or DeleteItem fails (ResourceNotFoundException on the table, AccessDenied, ConditionalCheckFailed, throttling) while S3 deletion succeeds.

Common situations: The DynamoDB lock table was renamed/deleted after lock acquisition; the IAM role lost dynamodb:DeleteItem; provisioned-capacity throttling under heavy concurrency; or the table is in a different region/account than the S3 bucket.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/bfb2cc9fdc189067. Report an issue: GitHub.