Deploying changing module dependencies with Drupal

Deploying changing module dependencies with Drupal

Difficulty: 
Let's Rock

Deployments are often one of the most important aspects of the Drupal development cycle. But sometimes, due to time and/or budget constraints (or the maturity of your company) developers clone databases downstream, manually reproduce content on production environments, and rely on other bad practices on a regular basis.

Today we will show you how we manage small (but critical) changes in module dependencies for our custom modules here at www.DrupalOnWindows.com.

Every custom module registers a version on startup and makes itself aware to our Fast Development Framework (FDF) in the .module file:

// Make this modules FDF aware
FdfCore::RegisterModule(new FdfModuleSettings(__FILE__, 1.0001));

When any of the modules has a critical change or update that needs to be run (we use a custom update framework different from hook_update) the version number must change.

A critical change is a configuration change or update that, if not deployed inmediately, will break the application.

For example, we have implemented custom code that uses the honeypot API to protect some user input. If the honeypot module is not enabled the moment this code is deployed, the site will break.

The more usual approach would be to take the site offline, deploy the new code, drush enable the module (or automate this through a hook_update_n and update) and take the site back online (well, with automated backups and so on...), and all this done by an external tool. But this approach has some drawbacks:

  • It relies on external tools to deploy (or trigger) the changes, requiring manual intervention when code is, for example, pulled to a development environment.
  • There is a downtime to the final user (not bad if done in a controlled manner, but you want to keep this to minimum in mission critical applications - for example in an e-learning platform you cannot afford a 1 minute downtime while someone is in the middle of an exam).

With FDF, this is all automated in a safe way inside Drupal. We only need to update the module version and add the dependency to the module's info file:

dependencies[] = honeypot

Inmediately after deploy FDF will detect there has been a change in the module version upon the first request (that is performed by our deployment tool or a very unlucky user) and will:

  • Lock all requests until changes have been deployed (yes, no offline, so this is for changes that will not take very long)
  • Detect missing updates and dependency changes (we are not talking here about hook_update updates)
  • Run missing updates and enable missing modules
  • Unlock requests and serve current page

It parses the info file:

$ini = drupal_parse_info_file(drupal_get_path('module', $module) . "/{$module}.info");
$modules = $ini['dependencies'];
// Sync dependencies.
\Drupal\fdf\FdfCore::module_update($modules);

And calls a custom syncronization method to enable missing modules:

/**
   * Syncs current enabled/disabled modules with provided list.
   *
   * Modules only uninstalled if $full_sync is set to true.
   *
   * @param mixed $modules 
   * @param mixed $sandbox 
   * @param mixed $full_sync 
   */
  public static function module_update($modules, &$sandbox = NULL, $full_sync = FALSE) {
    if (empty($sandbox)) {
      // When there is no sandbox, perform all in a single
      // operation.
      $current = module_list();
      $uninstall = array();
      
      // Solo desactivamos módulos si solicitamos FULL SYNC
      // de lo contrario solo activamos nuevos.
      if ($full_sync) {
        $uninstall = array_diff(array_keys($current), array_values($modules));
        // Remove the installation profile.
        $profile = drupal_get_profile();
        if(($key = array_search($profile, $uninstall)) !== false) {
          unset($uninstall[$key]);
        }
        // Remove all that are not required.
        module_disable($uninstall);
      }

      // Add missing modules.
      $missing = array_diff($modules, $current);
      foreach($missing as $module) {
        if (module_enable(array($module)) == FALSE) {
          throw new \Exception('Could not enable ' . $module . ' or one if its dependencies.');
        }
      }
      
      return;
    }
    
    $sandbox['#finished'] = 0;
    
    if (!isset($sandbox['module_update'])) {
      // Disable modules
      $current = module_list();
      
      $sandbox['module_uninstall']  = array();
      if ($full_sync) {
        $sandbox['module_uninstall']  = array_diff(array_keys($current), array_values($modules));
        // Remove the installation profile.
        $profile = drupal_get_profile();
        if(($key = array_search($profile, $sandbox['module_uninstall'])) !== false) {
          unset($sandbox['module_uninstall'][$key]);
        }
      }
      
      $sandbox['module_update']  = TRUE;
      $sandbox['module_install']  = $modules;
      $sandbox['module_total']  = count($sandbox['module_uninstall']) + count($sandbox['module_install']);
    }
    
    if (!empty($sandbox['module_uninstall'])) {
      foreach($sandbox['module_uninstall'] as $key => $module) {
        module_disable(array($module));
        unset($sandbox['module_uninstall'][$key]);
        break;
      }
    }
    else {
      foreach($sandbox['module_install'] as $key => $module) {
        if (module_enable(array($module)) == FALSE) {
          throw new \Exception('Could not enable ' . $module . ' or one if its dependencies.');
        }
        unset($sandbox['module_install'][$key]);
        break;
      }
    }
    $sandbox['module_remaining']  = count($sandbox['module_uninstall']) + count($sandbox['module_install']);
    $sandbox['#finished'] = ($sandbox['module_total'] - $sandbox['module_remaining']) / $sandbox['module_total'];
  }

Is this a good critical change deployment strategy? Depends on the type of change, the duration of the update and how bussy the site is.

And because the whole deployment process is inside Drupal, this works between dev environments without developers having to manually trigger anything.

Add new comment

By: david_garcia Sunday, March 22, 2015 - 00:00