Terraform/OpenTofu Fundamentals
What init and the Lock File Protect
In one line
.terraform.lock.hcl is a contract that records "what was allowed, what was chosen, and whether that is really that," and init is the command that checks this contract every time.
Why this file was needed
Providers are not inside the configuration file. Only the name and the version constraint are written, and init downloads the actual thing. So if two people who got the same repository run init at different times, they may end up holding different packages. If the provider releases a new version in between, one person plans with the old version and the other with the new version. If the plans differ, review loses its meaning.
You might say, why not just write a version constraint, but the constraint alone is not enough. A constraint usually allows a range. Which one within the range was chosen is not written in the constraint, and it also cannot tell whether the chosen package is the same bytes as the package downloaded yesterday. So a file arose that records three things separately.
provider "registry.opentofu.org/hashicorp/local" {
version = "2.9.0"
constraints = "2.9.0"
hashes = [
"h1:rxomJjDwOo+YZ+WIPc25FqEgsz9orh/2MCyUcZmFjvw=",
]
}
version is what was chosen, constraints is what was allowed, and hashes is the fingerprint of that package. The constraints line appears only when a version constraint was written in the configuration — it is absent altogether from the block of a provider with no constraint.
How it works
init moves in this order. It gathers the required providers and constraints from the configuration; if the lock file already has a choice, it tries to use that as it is, and if there is none or it conflicts with the constraint, it chooses anew. Before installing the chosen package, it compares it with the hashes in the lock file, and if they do not match, it refuses the installation.
A refusal looks like this.
Error: Failed to install provider
Error while installing hashicorp/local 2.9.0: the current package for
registry.opentofu.org/hashicorp/local 2.9.0 doesn't match any of the
checksums previously recorded in the dependency lock file
When this error appears, adding -upgrade does not clear it. Because if the version choice stays the same, it gets caught by the same check again. If a tampered hash is the cause, the honest recovery is to throw away the contract and make it again.
On the side of narrowing the version constraint, there is a hole people do not know well. The constraints line of the lock file is written when that entry is first created. If the version already chosen still fits the new constraint, init has no reason to choose again, so it does not write the lock file at all, and so a constraint attached later does not appear in the contract. The same goes if you add -upgrade and the choice stays the same. To reflect the allowed range in the contract, you have to make that entry be created anew — deleting the lock file and recreating it is the honest way.
It is a different story if nothing fits the range. Then it tries to choose again, and says there is nothing to choose.
Could not resolve provider hashicorp/local: no available releases match the
given constraints 2.5.0
There are two common ways to write a constraint. One is to pin an exact version, and the other is the tilde-arrow operator that leaves only the patch version open.
version = "2.9.0" # 정확히 이것만
version = "~> 2.9" # 2.x 안에서 2.9 이상, 3.0 미만
Another thing init makes is the .terraform/providers/ directory. Here the actual binaries are placed along a path that goes deeper in the order of registry address, namespace, name, version, and platform. This is an executable that differs by platform, so it is not committed. Init can recreate it at any time, so there is no loss in losing it. Conversely, the lock file is a change a person must review, so it is always committed — a provider version going up is as important an event as a code change.
There is also a command that creates or fixes the lock file without init. tofu providers lock does not install the package but only computes and writes the checksums. In an environment that looks only at an internal mirror, you can point at that mirror with -fs-mirror, and a team that uses the same repository on several platforms gives -platform several times to fill in the per-platform hashes in advance. If you use on a Mac a lock file that was init'ed only on Linux, it is blocked with "there is no hash for this platform," and this is the precaution against that.
What you see in the field
The most common accident is a repository that has put the lock file in .gitignore. Each person gets a different version, and one day when the provider changes a default, only one person's apply replaces resources. The second is fixing the lock file by hand when a merge conflict occurs. If a person edits the hash lines, nine times out of ten they go out of step, and from then on nobody can init on that branch. The standard is not to resolve the conflict by hand but to choose one side and then recreate it.
The third is when only CI fails. A developer's laptop has a cache left over, so it gets by even if the lock file is a bit off, but CI, which starts from an empty workspace every time, is blocked right away. So for a commit that changed the lock file, always run CI once and then merge.
What you will do in the next lab
You go through eight steps with the Pod's offline mirror. You open the lock file made by the first init and read the version and the number of hashes, pin the downloaded version as a constraint and confirm that a constraints line appears, and find the actual path of the installed package. Then you try making a plan without a lock file, try pinning a version that is not in the mirror and get rejected, and try recreating only the lock file without init. In the last two steps, you deliberately break a hash to confirm that installation is blocked and recover, and then build a check script that finds providers with no constraint.