Ensuring Consistent Configuration Across Drupal 7 Environments
A common issue that many Drupal developers have is maintaining consistent configuration across environments. Quite often, a developer may run into an issue where something that was tested and confirmed working in development fails in production, or vice versa. Typically, the issue stems from an undocumented change that was made in one environment but not the other. This is an adverse side effect of storing configuration within the database.
Ideally, all configuration should be under version control. In Drupal 8, this issue has been solved by the Configuration Management Initiative. However, to achieve the same goals in Drupal 7, one must implement workarounds.
For variables, it's rather trivial to enforce certain parts of your configuration. When it comes to the variable system, the $conf global has the final say on the value of a variable. The variable_get() function simply retrieves a variable from the global $conf array, and that array is built from the variables table with values in settings.php overriding the retrieved values. Basically, there are two ways to find out what variables on your site you can force via settings.php: the most thorough is to print out the $conf array (preferably through dpm()), but you can also search for instances of variable_set() and variable_get(). Looking for variable_get() might also expose undocumented but useful variables.
But what about more complex pieces of configuration, such as fields, Views, entity types, etc? In many cases, you can export those with the Features module. However, in order to be successful in using Features to manage the configuration of different parts of your site, you need to implement a policy that overridden Features modules in production should be considered broken.
But what about things that aren't exportable to Features? What about special steps that need to be taken to implement a bug fix or new feature? We like to leverage hook_update_N() within a custom deployment module. With this approach, we're able to define (and therefore easily document) changes in code (and therefore under version control) and simply use "drush updb" to implement them. (If you're not using Drush, and you really should be using Drush, this is the same as running update.php.) We put all of our configuration changes in one of these hooks in our deployment module, including variable_set(), module_enable(), module_disable(), features_revert() for any Features that we have changed, database queries to fix data issues caused by previous bugs, etc.
This has several advantages. For one, deployments become a lot simpler. Ideally, you want your deployments to be hands-off. What this method allows us to do is write a simple deployment script that pulls the new code from Git, put the site into maintenance mode, runs a registry rebuild via drush just in case we moved a module (some moves can cause white screens of death if you don't do this), runs updates via Drush, clears cache (just in case), runs cron (just in case), and takes the site back out of maintenance mode. In cases where we manage the ops side or the client's ops team allows, we'll also send a notification to the monitoring system to make sure that the new deployment is noted (very useful for determining when a problem was introduced).
Another key advantage is documentation. With configuration changes made via update hooks, we can tell exactly when configuration changes were introduced and by whom. It also all but eliminates the risk of a costly missed step on deployment to production as the deployment steps themselves are tested when a developer updates their development environment.
For some configuration settings, such as enabling a debug mode for a particular module, we may want to allow those changes to be made temporarily through the UI. However, accidentally leaving those debugging settings enabled can cause performance issues. In these cases, we combat this by defining critical settings in an implementation of hook_requirements(). (This only works for settings that have not been defined via the global $conf variable.) Using hook_requirements(), we're able to check that settings are appropriate for the environment (typically limited to production) and display a message to users with proper access if they are not in order to warn them that they need to adjust that setting back to the appropriate value.
But what should we do with hook_install()? There are two schools of thought here. One is to define all of your final configuration here, and the other is to loop over your implementations of hook_update_N(). I prefer the latter. While looping over hook_update_N() can be a more expensive process if your site has gone through some serious evolution, it's less duplication of code and less of a chance for you to miss something important. The goal of our use of hook_install() is to eliminate database cloning by only requiring the developer to install the deployment module to initialize a fully functional development version of the site.
Eliminate database cloning? For the most part, yes. Database cloning is a bad practice, even cloning upstream from production. Databases can be huge, and cloning a huge database can impact the performance of the site for your users. Databases can contain sensitive information (most standards dealing with the handling of sensitive information dictate that sensitive information only be accessible to those who absolutely need to access it), and most developers don't need access to sensitive information. Databases in production while a bug existed in production don't usually fit the requirement of presenting a known good starting point for development. Believe it or not, your content and customer information are not necessary for development.
So what can we do about content and other information? The answer is to generate it. Generate dummy content. Generate dummy orders. Generate dummy products. And so forth and so on. If you're doing automated testing, you will be having to do that anyway.
There are exceptions to not cloning the production database. Sometimes you will run into bugs that seem to only occur in production. In this case, it is acceptable to clone the database because the bug is likely caused by something that was unanticipated. However, when fixing these bugs you should take steps to ensure that whatever content or other information change that surfaced the bug is tested for before deploying to production thereafter.
Another exception is if you are using a staging and/or QA environment. In this case, you will want to clone the production database upstream to staging/QA just before deploying your fresh code to staging/QA. You need to do this to absolutely ensure that the changes in your deployment module cover all of the changes that will need to be made to production.
Finally, it's important to test. Writing the actual tests are outside the scope of this post, but it is important that you have automated tests in place that ensure that your site is configured exactly how you need it. Even if you have tests for your site's custom modules in those modules, you need to test your deployment module. The goal here is to fail fast. By having a separate test group for your configuration, you can have Jenkins (or whatever other continuous integration software you use) proceed to other tests only after your configuration tests have passed.