The practice of “pinning dependencies” refers to making explicit the versions of software your application depends on (defining the dependencies of new software libraries is outside the scope of this document). Dependency pinning takes different forms in different frameworks, but the high-level idea is to “freeze” dependencies so that deployments are repeatable. Without this, we run the risk of executing different software whenever servers are restaged, a new team-member joins the project, or between development and production environments. In addition to repeatability, pinning dependencies allows automatic notification of vulnerable dependencies via static analysis.
As such, all deployed applications should be pinning their library (and where possible: language, OS, etc.) versions. Let’s look at how to implement this in different languages.
No action is necessary for dependencies to be pinned. This is because the
Gemfile.lock should be committed to the repo
in development, causing it to be deployed along with the source code:
. . . the Gemfile.lock makes your application a single package of both your own code and the third-party code it ran the last time you know for sure that everything worked.
Package locking is built into npm >= 5. As you
npm install packages,
they’ll be added to your
package.json file and exact versions of all
libraries (including dependencies of dependencies) will be referenced in the
package.json and lock file should be
committed to the project repo.
In npm 6.x, the
npm ci command was introduced. This command will clear out
node_modules and install the exact dependency tree as defined in
package-lock.json. This is now the preferred method of ensuring dependencies
are pinned in CI/CD. npm 6 or greater is the default from Node.js 10.3.0.
Be sure to use an up-to-date npm 5.x client, as the lockfile behavior was
buggy in early versions. Use at least npm 5.4.2. Running
with no arguments will install the versions of libraries defined in the
npm < 5
npm < 5, you may imitate some of the above behavior by creating a
“shrinkwrap” file. As you install packages, use
npm install --save to update
package.json. After making changes, run
shrinkwrap to generate an
npm-shrinkwrap.json file, which references the versions of all the currently
npm install with no arguments will inspect that file and
install the versions it defines. Both the
package.json and shrinkwrap file
should be committed to the project repo.
If you are using
yarn to manage your node dependencies,
you will automatically have dependency pinning due to the
yarn produces and uses.
yarn.lock should be committed to your repository
All yarn.lock files should be checked into source control (e.g. git or mercurial). This allows Yarn to install the same exact dependency tree across all machines, whether it be your coworker’s laptop or a CI server.
pipenv install django # or, with stricter version bounds pipenv install django~=2.0.4
This will generate a
Pipfile containing a loose Django definition and a
Pipfile.lock referencing an exact Django version as well as all its
dependencies. Users need only run
pipenv install with no arguments to
synchronize the latest libraries.
Pipenv can also export a
requirements.txt file for tools that need one:
pipenv lock -r > requirements.txt
If Pipenv isn’t available, we can imitate some of its functionality by using
pip directly. We’ll create a
requirements.in file, specifying un-pinned
dependencies and install it via
pip install -r requirements.in
Then, we can “freeze” our libraries, generating a list of the exact versions of not only our immediate dependencies but their dependencies, by using:
pip freeze > requirements.txt
Be sure to run this command in an activated virtualenv to avoid freezing system-wide dependencies.
pip-tools provides a more automated
method of managing this flow.