hashicorp/terraform · error

failed to clean up file lock after DynamoDB lock error

Error message

failed to clean up file lock after DynamoDB lock error: %v; original error: %w

What it means

Only reachable when BOTH S3 native file locking and DynamoDB locking are enabled (useLockFile=true and ddbTable set). Terraform acquires the file lock first, then the DynamoDB lock; if the DynamoDB lock fails it attempts to roll back (release) the file lock. This error means the DynamoDB lock acquisition failed AND the compensating file-unlock also failed, so the system is left holding an S3 lock with no DynamoDB lock — the rollback itself broke.

Solutions

  1. Run `terraform force-unlock <id>` with the lock ID shown in the DynamoDB failure to clear the orphaned S3 lock file.
  2. Inspect the S3 lock file directly: `aws s3api get-object --bucket <bucket> --key <path>.tflock -` to confirm which client owns it and whether the ID matches.
  3. Verify the SSE-C customer key is stable across operations if customer_encryption_key is set — it must be identical for lock and unlock.
  4. Confirm the IAM principal still has s3:GetObject + s3:DeleteObject on the lock key at unlock time (a policy change mid-run is a common cause).
  5. If the DynamoDB lock was the original failure, address that first (it is the %w root cause); clearing DDB may be needed too.

Example fix

# clear the orphaned S3 lock file left behind by the failed rollback
terraform force-unlock <lock-id>

# or manually delete the .tflock object if the ID is unknown
aws s3api delete-object --bucket tf-state-prod --key prod/terraform.tflock.tflock
Defensive patterns

Strategy: retry

Validate before calling

// Validate symmetric access (lock + unlock) before enabling dual locking.
func validateDualLockAccess(ctx context.Context, s3c *s3.Client, ddb *dynamodb.Client, bucket, lockKey, table string) error {
  if _, err := s3c.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &bucket, Key: &lockKey}); err != nil {
    // NoSuchKey is fine; AccessDenied is not
    var apiErr smithy.APIError
    if errors.As(err, &apiErr) && apiErr.ErrorCode() != "NotFound" { return err }
  }
  if _, err := ddb.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {
    return err
  }
  return nil
}

Try / catch

// On rollback failure, surface BOTH errors and emit the lock ID so force-unlock is possible.
if unlockErr := c.unlockWithFile(ctx, info.ID, &statemgr.LockError{}, log); unlockErr != nil {
  return "", fmt.Errorf(
    "failed to clean up file lock after DynamoDB lock error: %v; original error: %w; " +
    "run `terraform force-unlock %s` to recover", unlockErr, err, info.ID)
}

Prevention

When it happens

Trigger: lockWithDynamoDB at client.go:333 fails AND the cleanup unlockWithFile at client.go:335 also fails. Triggers: DynamoDB ConditionalCheckFailed (someone else holds the DDB lock) combined with an S3 problem during cleanup (GetObject on the lock file fails, permissions revoked mid-run, or the lock file was concurrently deleted by another client).

Common situations: Two operators applying simultaneously where the loser hits DDB ConditionalCheckFailed and then a transient S3 blunder prevents cleanup; IAM permissions narrowed between the lock and unlock steps; SSE-C key rotated between operations so the cleanup GetObject fails decryption.

Related errors


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

Appendix: source

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

		log.Info("Attempting to lock remote state (DynamoDB only)...")
		if err := c.lockWithDynamoDB(ctx, info); err != nil {
			return "", err
		}

		log.Info("Locked remote state (DynamoDB only)")
		return info.ID, nil
	}

	// double locking: dynamodb + file (design decision: both must succeed)
	log.Info("Attempting to lock remote state (S3 Native and DynamoDB)...")
	if err := c.lockWithFile(ctx, info, log); err != nil {
		return "", err
	}

	if err := c.lockWithDynamoDB(ctx, info); err != nil {
		// Release the file lock if attempting to acquire the DynamoDB lock fails.
		if unlockErr := c.unlockWithFile(ctx, info.ID, &statemgr.LockError{}, log); unlockErr != nil {
			return "", fmt.Errorf("failed to clean up file lock after DynamoDB lock error: %v; original error: %w", unlockErr, err)
		}

		return "", err
	}

	log.Info("Locked remote state (S3 Native and DynamoDB)")
	return info.ID, nil
}

// lockWithFile attempts to acquire a lock on the remote state by uploading a lock file to Amazon S3.
//
// This method is used when the S3 native locking mechanism is in use. It uploads a lock file (JSON)
// to an S3 bucket to establish a lock on the state file. If the lock file does not already
// exist, the operation will succeed, acquiring the lock. If the lock file already exists, the operation
// will fail due to a conditional write, indicating that the lock is already held by another Terraform client.
func (c *RemoteClient) lockWithFile(ctx context.Context, info *statemgr.LockInfo, log hclog.Logger) error {
	lockFileJson, err := json.Marshal(info)
	if err != nil {

View on GitHub (pinned to d32a084675)