Modern dependency management for cloud-native apps
How to adapt dependency management—a classic app development factor—to thrive in cloud environments.
In Beyond the Twelve-Factor App, I present a new set of guidelines that builds on Heroku’s original 12 factors and reflects today’s best practices for building cloud-native applications. I have changed the order of some to indicate a deliberate sense of priority, and added factors such as telemetry, security, and the concept of “API first” that should be considerations for any application that will be running in the cloud. These new 15-factor guidelines are:
- One codebase, one application
- API first
- Dependency management
- Design, build, release, and run
- Configuration, credentials, and code
- Backing services
- Environment parity
- Administrative processes
- Port binding
- Stateless processes
- Authentication and authorization
Number 3 on the list, dependencies, refers to the management of application dependencies: how, where, and when they are managed. In this article I explore this classic concept through a cloud-native lens.
Reliance on the Mommy Server
In classic enterprise environments, we’re used to the concept of the mommy server. This is a server that provides everything that our applications need and takes care of their every desire, from satisfying the application’s dependencies to providing a server in which to host the app. The inverse of a mommy server, of course, is the embedded, or bootstrapped, server, where everything we need to run our application is contained within a single build artifact.
The cloud is a maturation of the classic enterprise model, and as such, our applications need to grow up to take advantage of the cloud. Applications can’t assume that a server or application container will have everything they need. Instead, apps need to bring their dependencies with them. Migrating to the cloud, maturing your development practices, means weaning your organization off the need for mommy servers.
If you’ve been building applications in languages or frameworks that don’t rely on the container model (Ruby, Go, Java with Spring Boot, etc.), then you’re already ahead of the game, and your code remains blissfully unaware of containers or mommy servers.
Modern dependency management
Most contemporary programming languages have some facility for managing application dependencies. Maven and Gradle are two of the most popular tools in the Java world, while NuGet is popular for .NET developers, Bundler is popular for Ruby, and godeps is available for Go programmers. Regardless of the tool, these utilities all provide one set of common functionality: they allow developers to declare dependencies and let the tool be responsible for ensuring that those dependencies are satisfied.
Many of these tools also have the ability to isolate dependencies. This is done by analyzing the declared dependencies and bundling (also called vendoring) those dependencies into some sub-structure beneath or within the application artifact itself.
A cloud-native application never relies on implicit existence of system-wide packages. For Java, this means that your applications cannot assume that a container will be managing the classpath on the server. For .NET, this means that your application cannot rely on facilities like the Global Assembly Cache. Ruby developers cannot rely on gems existing in a central location. Regardless of language, your code cannot rely on the pre-existence of dependencies on a deployment target.
Not properly isolating dependencies can cause untold problems. In some of the most common dependency-related problems, you could have a developer working on version X of some dependent library on his workstation, but version X+1 of that library has been installed in a central location in production. This can cause everything from runtime failures all the way up to insidious and difficult to diagnose subtle failures. If left untreated, these types of failures can bring down an entire server or cost a company millions through undiagnosed data corruption.
Properly managing your application’s dependencies is all about the concept of repeatable deployments. Nothing about the runtime into which an application is deployed should be assumed that isn’t automated. In an ideal world, the application’s container is bundled (or bootstrapped, as some frameworks called it) inside the app’s release artifact—or better yet, the application has no container at all.
However, for some enterprises, it just isn’t practical (or possible, even) to embed a server or container in the release artifact, so it has to be combined with the release artifact, which, in many cloud environments like Heroku or Cloud Foundry, is handled by something called a buildpack.
Applying discipline to dependency management will bring your applications one step closer to being able to thrive in cloud environments.