In the twelfth and final post in our twelve-part DevOps series, we bring it all home and delve into why it’s important to version control everything in development projects.
Factor 12: Version Everything
The last of the twelve factors is to version control everything. This includes not only your application code, but also your infrastructure, configuration management, and database schema code. For developers this is common practice; for operations-minded staff it can be a fundamental change.
Infrastructure as code (IaC) is not a new concept, but there are more tools than ever to produce it cleanly and efficiently in the cloud-native world. 12-factor DevOps infrastructure must never be bespoke. Not only is there no excuse for hand-crafted resources, but for your infrastructure to truly have parity you have no choice but to embrace IaC. UI- or one-off CLI-based changes are notoriously difficult to track and troubleshoot, even with the most finely-tuned change advisory processes. Well-written IaC brings visibility and clarity to the infrastructure and aids in disaster-recovery. Properly executed, it shortens the time necessary to deploy a full replacement environment and get it running from days to hours or even minutes.
Every resource you use, from a simple virtual machine or database instance to load-balancers, data lakes, and serverless functions must be defined as code, as must their configurations. That code must be checked into version control and scrutinized the same way as the application code would be. Even when you are working with bare-metal resources, you can still define the infrastructure as code to an extent. You may not be able to define the compute or storage resources directly, but you do have control over the software installation and configuration via scripting and tools like the aforementioned Terraform, Ansible, and Puppet.
Database schemas, too, should be defined as code and never hand-crafted for the same reasons as the infrastructure itself; if the whole team is not aware that a change happened or how it was made, tracking down any issues related to it becomes an ordeal. Version-controlled database schemas help ensure that the database matches the application version, and they can be automatically applied during the deployment of the application. Tools such as Liquibase, Redgate, and Flyway are purpose-built for database work, but you can just as easily use Terraform or Ansible. Which tool you pick is a function of your environment and your needs for unifying your toolkit.
When a new resource is required or a change to an existing one needs to be made, a feature branch must be checked out prior to any code changes. The code is updated, then committed and pushed. No change to the code should be permitted to merge without peer review and automated testing. Though you wouldn’t apply the changes during these tests, you would lint the code and use the validation and planning tools provided by the IaC application to ensure only the desired changes will occur. After the tests pass and the changes are approved, you can merge.
Upon merger, a new version of the code should be created and applied (ideally by your automation engine). If the deployment fails for any reason, you should strive to fix-forward by applying a new set of changes rather than reverting to the older version. The older versions exist to show the environmental changes between iterations, not as a means of reverting to an earlier state.
All of this works together to help you build reliable, repeatable infrastructure where your applications can flourish. If you have existing resources, you can start by using IaC tools to define new ones. Once you’re comfortable with the process, many of the IaC tools have ways for you to import existing resources so you don’t need to worry about completely rebuilding what you already have.