hashicorp/terraform · error · statemgr.LockError

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

Error message

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

What it means

Thrown by RemoteClient.Unlock when both S3 lock-file deletion (unlockWithFile) AND DynamoDB lock-item deletion (unlockWithDynamoDB) fail simultaneously while running in the dual-locking mode (useLockFile=true and ddbTable set). The wrapped message concatenates the two underlying AWS errors so the operator can see why each backend rejected the delete. Because the lock is held by two independent stores, a partial infrastructure outage or permission scope can cause both to fail at once.

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 c9def3e214)

Solutions

  1. Inspect the wrapped 'S3 error' and 'DynamoDB error' substrings to identify which AWS API failed for each store and address the more actionable one first.
  2. Verify the running credentials (aws sts get-caller-identity) and confirm they have both s3:GetObject+DeleteObject on the lock key and dynamodb:GetItem+DeleteItem on the table.
  3. Re-run the operation once the IAM/credential issue is fixed; the lock file and item still exist so Unlock can retry.
  4. If the lock is stuck and credentials are correct, manually delete the S3 <lockFilePath> object and the DynamoDB row with LockID = <bucket>/<path> using aws-cli.

Example fix

// before: principal lacks dynamodb:DeleteItem
// after: IAM policy includes
{
  "Effect": "Allow",
  "Action": ["dynamodb:DeleteItem", "dynamodb:GetItem"],
  "Resource": "arn:aws:dynamodb:*:*:table/my-lock-table"
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: confirm principal can reach and write both lock stores
import (
  "github.com/aws/aws-sdk-go-v2/service/s3"
  "github.com/aws/aws-sdk-go-v2/service/dynamodb"
)

func preflightLockStores(ctx context.Context, s3c *s3.Client, ddbs *dynamodb.Client, bucket, lockKey, table, lockID string) error {
  if _, err := s3c.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &bucket, Key: &lockKey}); err != nil {
    return fmt.Errorf("s3 preflight: %w", err)
  }
  if _, err := ddbs.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {
    return fmt.Errorf("dynamodb preflight: %w", err)
  }
  return nil
}

Try / catch

// Unlock can return a *statemgr.LockError; inspect .Err and .Info
import "github.com/hashicorp/terraform/statemgr"

err := client.Unlock(id)
if le, ok := err.(*statemgr.LockError); ok {
    log.Printf("unlock failed: %v (held by %s)", le.Err, le.Info.Who)
    // branch on wrapped error codes before retrying
}

Prevention

When it happens

Trigger: Calling Unlock(id) on an S3 backend configured with both useLockFile=true and a dynamodb_table, while the IAM principal lacks s3:DeleteObject on the .tflock key AND dynamodb:DeleteItem on the table, or while both AWS services are unreachable (e.g. network partition, expired STS credentials).

Common situations: Running 'terraform force-unlock' or an apply/destroy cleanup after the IAM policy was tightened, an STS session expired mid-operation, a bucket policy started denying the principal, or a shared CI role lost access to one of the two resources. Also seen during cross-account setups where one role has S3 access but not DynamoDB.

Related errors


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