hashicorp/terraform · error

lock file not exists

Error message

lock file %s not exists

What it means

Thrown by the COS remote-state backend's lockInfo() helper when the lock object is queried (exists==false) with no transport error. It means the lock file the backend expects to find in the Tencent COS bucket is absent, so the stored LockInfo cannot be read. This typically surfaces inside Unlock()/lockError() paths when Terraform tries to reconcile a lock that was never written, was already removed, or was deleted out of band.

Solutions

  1. Verify the lock object exists in the bucket: list objects under the configured prefix/key (default is `<state key>.tflock`) via the COS console or API.
  2. If the lock file truly is gone but Terraform still thinks state is locked, run `terraform force-unlock <LOCK_ID>` with the ID from the prior lock attempt.
  3. Confirm the backend `key`/`state` and `lock_file` (or prefix) settings match the run that originally locked the state.
  4. If another run legitimately holds the lock, wait for it to finish or have it release the lock rather than deleting the file manually.

Example fix

// before: backend block with mismatched key leading to wrong lock path
backend "cos" { bucket="tf-state"; key="prod/terraform.tfstate"; lock_file="prod/terraform.tfstate.tflock" }
// after: ensure lock_file path aligns with the actual object key and remove stale references
backend "cos" { bucket="tf-state"; key="prod/terraform.tfstate" }  // lock_file defaults to <key>.tflock
Defensive patterns

Strategy: validation

Validate before calling

// Before attempting Unlock, check the lock object exists via the same backend
// (run from a small Go helper or a terraform command):
//   terraform state list  # will surface the lock state during plan
// In custom automation wrapping Terraform, only call force-unlock when the
// lock file is verifiably absent:
func lockFileExists(cosClient *cos.Client, ctx context.Context, lockFile string) (bool, error) {
    rsp, err := cosClient.Object.Get(ctx, lockFile, nil)
    if err == nil && rsp != nil && rsp.StatusCode == 200 {
        rsp.Body.Close()
        return true, nil
    }
    if rsp != nil && rsp.StatusCode == 404 {
        rsp.Body.Close()
        return false, nil
    }
    return false, err
}

Type guard

// Guard a force-unlock decision on the verifiable absence of the lock object
func shouldForceUnlock(cosClient *cos.Client, ctx context.Context, lockFile string) bool {
    exists, err := lockFileExists(cosClient, ctx, lockFile)
    return err == nil && !exists
}

Try / catch

// Go callers of the COS backend should treat errors.Is on the wrapped message
// or, better, check statemgr.LockError type:
if errors.As(err, &lockErr *statemgr.LockError) {
    // inspect lockErr.Info; if nil and message contains 'not exists', the lock
    // file is already gone — treat unlock as best-effort/no-op
}

Prevention

When it happens

Trigger: Calling remoteClient.Unlock() (which calls lockInfo()) when no lock object exists at c.lockFile in the COS bucket; or lockError() trying to enrich a prior failure by reading a lock file that is gone. Also reached if a concurrent run/another process/manual deletion removed the .tflock file between Lock() and Unlock().

Common situations: A previous `terraform apply` crashed or was killed before writing the lock file; someone manually deleted the lock object from the COS bucket; the lock file path (state/lock prefix) was changed in backend config between operations; a stale lock ID is being passed to `terraform force-unlock`.

Related errors


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

Appendix: source

Thrown at internal/backend/remote-state/cos/client.go:168

	info, infoErr := c.lockInfo()
	if infoErr != nil {
		lockErr.Err = errors.Join(lockErr.Err, infoErr)
	} else {
		lockErr.Info = info
	}

	return lockErr
}

// lockInfo returns LockInfo from lock file
func (c *remoteClient) lockInfo() (*statemgr.LockInfo, error) {
	exists, data, checksum, err := c.getObject(c.lockFile)
	if err != nil {
		return nil, err
	}

	if !exists {
		return nil, fmt.Errorf("lock file %s not exists", c.lockFile)
	}

	info := &statemgr.LockInfo{}
	if err := json.Unmarshal(data, info); err != nil {
		return nil, err
	}

	info.ID = checksum

	return info, nil
}

// getObject get remote object
func (c *remoteClient) getObject(cosFile string) (exists bool, data []byte, checksum string, err error) {
	rsp, err := c.cosClient.Object.Get(c.cosContext, cosFile, nil)
	if rsp == nil {
		log.Printf("[DEBUG] getObject %s: error: %v", cosFile, err)
		err = fmt.Errorf("failed to open file at %v: %v", cosFile, err)

View on GitHub (pinned to d32a084675)