How repeatable deployments work
One of the key selling points of Platform.sh is predictable, repeatable deployments. We view the read-only production file system as a key advantage for our customers. But why is that, and how do we manage it? Just how repeatable and predictable is it?
The key to reliability is predictability. If I know my code works in testing, I should be able to predict, with a very high degree of confidence, that it will work in production, too. To do that, we need to minimize the number of variables that differ between staging and production. Zero is ideal, but anything close to zero is generally good enough.
At Platform.sh, the build process for all environments, production or not, is exactly the same. And the inputs to that build process are all consistent and largely predictable.
Overall there are four inputs to any running application:
- Your code, as tracked by Git. That includes any configuration directives in Platform.sh configuration files, your php.ini file, etc.
- Any 3rd party dependencies that are downloaded at build time, such as from Composer, npm, or Ruby gems.
- Any configuration variables defined in a project via the UI.
- The underlying container image we provide.
If those inputs are all the same, the output should be identical. (Functional programming DevOps?) So when can those change?
First of all, let’s clarify exactly how an application container works. There are three parts to every container: The container image, the application image, and the writeable file system.
The container image is provided by us, and contains nginx, the runtime appropriate for your application (PHP, Python, etc.), and custom tools we built to manage our orchestration. It is shipped as a read-only file system, using squashfs. It gets mounted at
The application image is the result of running your build hook on the code in your git repository. The resulting files are compressed into a read-only file system, using squashfs, and mounted at
The writeable file system is specified by your
.platform.app.yaml file, and can be zero or more locations that get mounted as writeable disks at a directory (under
/app) that you specify.
When Platform.sh “deploys” your container, it means we:
- “Close” the application container so it doesn’t accept new requests
- Shutdown the container
- Replace the container image with a new version, if there is one
- Replace the application image
- Mount the writeable file system
- Inject any configured environment variables
- Run user-specified deploy hooks
- ”Open” the application container to accept requests again
Assembling the container.
Now that we know how a container works, let’s look at those inputs again.
Your code in Git, of course, only changes when you tell it to. That’s the point of Git. It changes only when you want it to change. It changes only on the branch you want to change. You can merge to another branch, at which point that branch has changed because you wanted it to change. It’s entirely under your control.
3rd party code
3rd party dependencies depend primarily on the configuration in your code, in Git. Most package managers these days support a lock file (such as
composer.lock for PHP) that will ensure that the exact same code is downloaded every time, even if a newer version is available. That’s why you should always check your lock file into your Git repository for an application. That way, your 3rd party dependencies are entirely under your control.
Environment variables are configured by you, through either the Platform CLI or the web interface. They can be any values you want, but are mostly useful for things like API keys, login credentials, and other values that should not be in Git (either for security reasons or because they need to be different between a Platform.sh environment and your local environment). Because these are set by you, the user, they’re entirely under your control.
The container image
The container image is the only input that is controlled by Platform.sh, not by the user. We currently have over a dozen container images for various languages and language versions. We only release new versions of these when there’s a new version of our integration software available or there’s a bug or security fix in one of the tools we include. That means new bugfix or security releases of PHP or Python, or an update to nginx, for instance. We will never change your container type out from under you (say, switching from PHP 5.5 to 7.0), but we may update the type as needed (such as the monthly security release of PHP 7.0.x).
Of particular note is that everything that goes into the application image (your code and 3rd party code) is controlled by Git, and Git is controlled by you, thus the application image is based exclusively on things within your control. It also means it’s predictable for us, too: If the hash of all files in a given branch already exists, and we’ve already built a container image for it, then we know, with certainty, that the resulting application image would be the same, too. That means we don’t even rebuild it, saving time and resources. (We don’t use the git hash directly for that, as if you have multiple application containers we want to treat them separately.)
It also guarantees consistency for your deployment. Suppose you have a branch
feature-x on which you’ve done some development and deployed it to a Platform.sh dev environment. That means we’ve built an application image out of it. Now you merge it back to
master. As long as the merge is a fast-forward merge in Git, the files that were at the HEAD of the
feature-x branch will now be identical to the
masterbranch. Identical files means we can reuse the existing application image.
Your application image in production isn’t just like the one that was in your feature branch. It is the one that was in your feature branch. The same exact bits.
What can still vary
While the application image is reused as-is, the container image may get a new version if one is available. That will only be different from the dev branch if a new version has been released between when you last pushed to that branch and when you merged it to
master. The odds of that are low, but if you want to be certain you can always add an empty commit to the dev branch to trigger a deploy, which would pick up a new container image if one exists. If all is well, merge to
master with confidence.
The writeable file system may also vary between production and a development environment. When a new environment is created its data is cloned from production, but depending on how long your dev environment lives before getting merged the two could diverge. Again, if you want to be sure just click the “Sync” button in the UI to copy production’s data to your dev environment and check it over. It only takes a minute or two, regardless of how much data you have.
Your environment variable configuration may also vary between environments, but in that case it’s generally because you want it to; you don’t want your development instance sending purchase orders with real credit cards against a real payment gateway. (At least, we assume not.)
So there you have it: Absolutely identical code between development and production, and usually absolutely identical environments (that can be guaranteed identical if you want them to be). It’s as close to functional programming as DevOps can get, and it’s just another day at the office around here.