Using Drupal Configuration Management to build an app

There's a lot to say about Drupal Configuration Management. Many contrib modules have emerged to address the shortcomings in core and I agree that most of them are clearly solving a need. I even have colleagues claiming "there's a CM-related article every week on Drupal Planet!". Here's one more :-)

Still, I'm trying to work with what core has to offer unless forced to do otherwise. And you can already do a ton. Seriously. What I think is crucial with CM is the possibility to 'productize' or 'featurize' a site's configuration. Think building an app from scratch through automation. Think SaaS. Put differently, it's all about being able to build a specific feature (e.g. content type, form/view mode, etc.) and ship it to any other D8 instance.

Yes, the idea here is not to solve the dev to stage to prod deployment issues but to primarily spin up a new D8 dev instance and configure it like a full-featured application. And core does that very well out of the box.

Building the app

Back in 2014 when I started learning D8 and built a PoC of a REST-based D8 endpoint, I had to wipe my test site almost daily and create it from scratch again as core was constantly changing. Then I realized CM was perfect for this use case. Back then I had to work around UUID issues. Allow a site to be installed from existing configuration demonstrates our headache isn't over just yet. But the concept was essentially the same as it is today:

  • Spin up a new D8 instance
  • Enable all required contrib/custom modules/themes
  • Export your site's configuration
  • Version-control your CM sync directory
  • Add all CM files under version control
  • Build a simple feature (e.g. content type)
  • Export your site's configuration
  • Copy the new/modified files for later use (thanks git diff)
  • Add all new/modified CM files under version control
  • Rinse & repeat

With this simple workflow, you'll be able to incrementally build a list of files to re-use when building a new D8 instance from scratch. Oh, and why would we even bother creating a module for that? This works great as it is, granted you'll be extra careful (TL;DR use git) about every change you make.

Spinning up a new app

To test setting up your app, the workflow then becomes:

  • Spin up a new D8 instance
  • Enable all required contrib/custom modules/themes
  • Export your site's configuration
  • Version-control your CM directory
  • Add all CM files under version control
  • Copy your previously backed up configuration files to the sync directory
  • Import your new configuration

Looking back to how life was before Drupal 8, you will likely not disagree this is much better already. Here's an example for building an app from scratch. All of this could obviously be scripted.

$ cd /path/to/sync/dir
$ for i in module1, module2, module3, module4, module5 ; do drush @site.env en -y $i ; done
$ drush @site.env cex -y
$ git init
$ git add --all && git commit -m "Initial configuration"
$ cp /path/to/configuration/backup/*.yml .
$ git status
$ drush @site.env cim -y
$ git add --all && git commit -m "New configuration"

Now, here's a real-life example, summarized through the bit we're interested in: building the app from scratch through config-import.

$ drush @d8.local cim -y
    Config                                           Operation                                                                                             
    field.storage.node.field_inline_client              create 
    field.storage.node.field_email                      create 
    field.storage.node.field_address                    create 
    node.type.client                                    create 
    field.field.node.client.field_email                 create 
    field.field.node.client.field_address               create 
    core.base_field_override.node.client.title          create 
    node.type.contract                                  create 
    field.field.node.contract.field_inline_client       create 
    core.base_field_override.node.contract.title        create 
    core.base_field_override.node.contract.promote      create 
    field.storage.paragraph.field_unit                  create 
    field.storage.paragraph.field_reference             create 
    field.storage.paragraph.field_quantite              create 
    field.storage.paragraph.field_price                 create 
    field.storage.node.field_service                    create 
    field.field.node.contract.field_service             create 
    core.entity_form_display.node.contract.default      create 
    paragraphs.paragraphs_type.service                  create 
    field.field.paragraph.service.field_unit            create 
    field.field.paragraph.service.field_reference       create 
    field.field.paragraph.service.field_quantite        create 
    field.field.paragraph.service.field_price           create 
    field.storage.node.field_telephone                  create 
    field.field.node.client.field_telephone             create 
    core.entity_form_display.node.client.default        create 
    field.storage.paragraph.field_description           create 
    field.field.paragraph.service.field_description     create 
    core.entity_view_display.paragraph.service.default  create 
    core.entity_form_display.paragraph.service.default  create 
    core.entity_view_display.node.contract.teaser       create 
    core.entity_view_display.node.contract.default      create 
    core.entity_view_display.node.client.default        create 
    user.role.editor                                    create 
    system.action.user_remove_role_action.editor        create 
    system.action.user_add_role_action.editor           create 
    auto_entitylabel.settings                           create
Import the listed configuration changes? (y/n): y  
 [notice] Synchronized configuration: create field.storage.node.field_inline_client.
 [notice] Synchronized configuration: create field.storage.node.field_email.
 [notice] Synchronized configuration: create field.storage.node.field_address.
 [notice] Synchronized configuration: create node.type.client.
 [notice] Synchronized configuration: create field.field.node.client.field_email.
 [notice] Synchronized configuration: create field.field.node.client.field_address.
 [notice] Synchronized configuration: create core.base_field_override.node.client.title.
 [notice] Synchronized configuration: create node.type.contract.
 [notice] Synchronized configuration: create field.field.node.contract.field_inline_client.
 [notice] Synchronized configuration: create core.base_field_override.node.contract.title.
 [notice] Synchronized configuration: create core.base_field_override.node.contract.promote.
 [notice] Synchronized configuration: create field.storage.paragraph.field_unit.
 [notice] Synchronized configuration: create field.storage.paragraph.field_reference.
 [notice] Synchronized configuration: create field.storage.paragraph.field_quantite.
 [notice] Synchronized configuration: create field.storage.paragraph.field_price.
 [notice] Synchronized configuration: create field.storage.node.field_service.
 [notice] Synchronized configuration: create field.field.node.contract.field_service.
 [notice] Synchronized configuration: create core.entity_form_display.node.contract.default.
 [notice] Synchronized configuration: create paragraphs.paragraphs_type.service.
 [notice] Synchronized configuration: create field.field.paragraph.service.field_unit.
 [notice] Synchronized configuration: create field.field.paragraph.service.field_reference.
 [notice] Synchronized configuration: create field.field.paragraph.service.field_quantite.
 [notice] Synchronized configuration: create field.field.paragraph.service.field_price.
 [notice] Synchronized configuration: create field.storage.node.field_telephone.
 [notice] Synchronized configuration: create field.field.node.client.field_telephone.
 [notice] Synchronized configuration: create core.entity_form_display.node.client.default.
 [notice] Synchronized configuration: create field.storage.paragraph.field_description.
 [notice] Synchronized configuration: create field.field.paragraph.service.field_description.
 [notice] Synchronized configuration: create core.entity_view_display.paragraph.service.default.
 [notice] Synchronized configuration: create core.entity_form_display.paragraph.service.default.
 [notice] Synchronized configuration: create core.entity_view_display.node.contract.teaser.
 [notice] Synchronized configuration: create core.entity_view_display.node.contract.default.
 [notice] Synchronized configuration: create core.entity_view_display.node.client.default.
 [notice] Synchronized configuration: create user.role.editor.
 [notice] Synchronized configuration: create system.action.user_remove_role_action.editor.
 [notice] Synchronized configuration: create system.action.user_add_role_action.editor.
 [notice] Synchronized configuration: create auto_entitylabel.settings.
 [notice] Finalizing configuration synchronization.
 [success] The configuration was imported successfully.

If the import was successful, reload your site and observe everything shows up like magic: site configuration, content types, custom fields, view/form modes, module configuration, views, etc.

Sure you could argue that doing so is very prone to errors, but remember that a) it's primarily for development needs and b) you need to version-control all of this to be able to go back to the last working commit and revert if necessary.

Wrapping up

Two other use cases I very much like are:

  • When I want to demonstrate an issue, I can simply share some files for someone else to import and quickly reproduce the issue with little efforts and no room for configuration mismatch.
  • When building a new feature (e.g. a Flag and a View), I can do so in dev, then export only the files I need, and import in stage or prod when I'm ready.

Building an app will obviously take much more than that but, as I hear more and more frustration about how Configuration Management was designed, I thought I'd set the record straight on how it solves my biggest problems when developing an app.

Aurelien Navarre

Senior Technical Solutions Analyst @Acquia - Drupalist by day, DevOps by night.

Lyon, France