hashicorp/terraform · error

failed to unlock DynamoDB

Error message

failed to unlock DynamoDB: %v

What it means

Dual-lock unlock where the S3 file unlock succeeded but the DynamoDB unlock (unlockWithDynamoDB) failed. The S3 `.tflock` is gone but the DynamoDB LockID row remains, so the state is half-unlocked and other clients may still see the DDB lock. lockErr carries the wrapped DynamoDB failure.

Solutions

  1. Manually delete the stale DynamoDB row: `aws dynamodb delete-item --table-name <table> --key '{"LockID":{"S":"<bucket>/<path>"}}'`.
  2. Confirm the table still exists in the configured region/profile: `aws dynamodb describe-table --table-name <table>`.
  3. Verify dynamodb:GetItem + dynamodb:DeleteItem permissions on the table for the principal.
  4. If throttling, switch the lock table to on-demand billing or raise write capacity, then retry force-unlock.
  5. Re-run `terraform force-unlock <id>` once the underlying DDB access is restored.

Example fix

# S3 lock cleared but DynamoDB row lingers — remove it
aws dynamodb delete-item \
  --table-name terraform-locks \
  --key '{"LockID":{"S":"tf-state-prod/prod/terraform.tfstate"}}'
Defensive patterns

Strategy: retry

Validate before calling

// Validate DynamoDB lock-table access before unlock.
func canUnlockDDB(ctx context.Context, c *dynamodb.Client, table, lockID string) error {
  if _, err := c.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {
    return err
  }
  return nil
}

Try / catch

// On DDB-only unlock failure, retry, then surface the lock ID + row key for manual cleanup.
if derr != nil {
  if err2 := c.unlockWithDynamoDB(ctx, id, lockErr); err2 == nil { derr = nil }
}
if derr != nil {
  lockErr.Err = fmt.Errorf("failed to unlock DynamoDB: %v; delete row LockID=%s in table %s", derr, lockPath, ddbTable)
  return lockErr
}

Prevention

When it happens

Trigger: unlockWithDynamoDB returns an error while unlockWithFile succeeded. Triggers: getLockInfoWithDynamoDB fails (GetItem error, table gone, AccessDenied), the DDB lock ID does not match the provided id, or the final DeleteItem on the DDB row fails (throttling, permissions revoked, table deleted between lock and unlock).

Common situations: DynamoDB table deleted or renamed after the lock was taken, IAM principal lost dynamodb:DeleteItem mid-run, provisioned-capacity throttling on the lock table, or a concurrent force-unlock already cleared the DDB row so getLockInfoWithDynamoDB returns no match.

Related errors


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

Appendix: 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 d32a084675)