March 21, 2020

Class Containment in Puppet Enterprise

How to & Use Cases
Products & Services

Class containment is an advanced topic in Puppet, and something that new users often find confusing. In the past, elaborate systems of anchor resources needed to be created to order classes effectively, and this sometimes led to difficult-to-diagnose dependency cycles.

The purpose of containment, in general, is to let you control where and when certain parts of your Puppet code are executed. Containment offers granular control by operating on the level of individual resources, and it also offers tremendous power by operating at the class level.

But let’s step back for a minute.

Table of Contents

What Is Class Containment?

Containment is the policy of Puppet to prevent the spread of “ssh-and-a-for-loop” in IT shops throughout the world by rogue sysadmins.

Just kidding.

Containment, put simply, is the relationships that resources have to the classes and defined types that "contain" them.

Classes and defined type instances contain the resources they declare. Any contained resources will not be applied before the container is begun, and will be finished before the container is finished.

So for example, if we run puppet apply with the following manifest:

#example1.pp
notify { 'begin':
  before => Class['myklass'],
}

class myklass {
  notify { 'foo': }
  notify { 'bar': }
  notify { 'baz': }
}

notify { 'end':
  require => Class['myklass'],
}

include myklass

we receive the following output:

Notice: begin
Notice: /Stage[main]//Notify[begin]/message: defined 'message' as 'begin'
Notice: baz
Notice: /Stage[main]/Myklass/Notify[baz]/message: defined 'message' as 'baz'
Notice: foo
Notice: /Stage[main]/Myklass/Notify[foo]/message: defined 'message' as 'foo'
Notice: bar
Notice: /Stage[main]/Myklass/Notify[bar]/message: defined 'message' as 'bar'
Notice: end
Notice: /Stage[main]//Notify[end]/message: defined 'message' as 'end'
Notice: Finished catalog run in 0.08 seconds

You'll notice that the begin notify resource fires before all the notify resources in the class myklass, just as the end notify resource is applied after all of the notify resources in the class myklass. This is because the relationships that the begin and end notify resources have with the class myklass apply to the resources within myklass as well. This is "containment".

If you're using a version of Puppet Enterprise prior to 3.2.0, or Open Source Puppet earlier than 3.4.0, please refer to the section lower down in this post with the subhead, "Class Containment Prior to Puppet Enterprise 3.2.0 (or Open Source Puppet 3.4.0)."

What is Class Declaration?

Where this gets more complicated, and often leads to confusion is with class declaration.

Given the following example, what would you expect to happen?

class first {
  notify { 'foobarbaz': }
}

class second {
  notify { 'bazbarfoo': }
}

class classa {
  include first
}

class classb {
  include second
}

Class['classa'] -> Class['classb']

include classa
include classb

It looks like the notify { 'foobarbaz': } resource will be applied before notify { 'bazbarfoo': } due to the class ordering, but this turns out to not be the case:

Notice: bazbarfoo
Notice: /Stage[main]/Second/Notify[bazbarfoo]/message: defined 'message' as 'bazbarfoo'
Notice: foobarbaz
Notice: /Stage[main]/First/Notify[foobarbaz]/message: defined 'message' as 'foobarbaz'
Notice: Finished catalog run in 0.23 seconds

This is because classes nevercontain the classes they include. So even though classa and classb are ordered, the classes they include are not.

It can be a little confusing, because the words "contain" and "include" are so similar, but they have different meanings in Puppet syntax.

Why is this the case? Well, for one thing, classes can be included multiple times. Imagine for example the following scenario:

class a {
  include ntp
  notify { 'I am class a.':}
}

class b {
  include ntp
  notify { 'I am class b.':}
}

include a
include b

Class['a'] -> Class['b']

If classes were contained each time they were included, Puppet would have no way of knowing what order to apply the resources within the ntp class, in the above example. Therefore, in order to allow classes to be included an unlimited number of times per node, classes nevercontain the classes they include. Because classes don't contain other classes, Puppet will happily apply the catalog generated from the manifest above, including the ntp class.

When Should I Care About Class Containment?

Whenever you care about forming ordering relationships against a module that contains subclasses, you likely care about class containment. For example, if you are building an Apache module that you plan to reuse multiple times, and it contains a top level apache class that can be declared directly, as well as several subclasses (think apache::package, apache::file, apache::service, but it can often be much more complex than that). If you want those subclasses to be affected by ordering against the main class, you'll need to deal with class containment.

This comes into play especially when you're writing modules you'll want to re-use, like a generic MySQL module. For example, if you look at the puppetlabs/mysql module on GitHub, you'll see that the class that manages the installation of the server components uses the "anchor pattern" (more on that below), to contain its subclasses. This allows you to write other modules that can reliably depend on MySQL being configured in a certain order during the Puppet run, either before or after other resources and classes.

Dealing with Class Containment

Let's imagine that we're spinning up a web stack on a single node, that includes a database like MySQL and a web server like Apache.

We've written manifests to manage all this, but it's critical in our infrastructure that the database be operational before the web server is brought online, so that the web application can actually function.

To that end, we've written the following Puppet code:

# /etc/puppetlabs/puppet/modules/profiles/manifests/dbserver.pp
class profiles::dbserver {
  include mysql
}

# /etc/puppetlabs/puppet/modules/profiles/manifests/webserver.pp
class profiles::webserver {
  include apache
}

# /etc/puppetlabs/puppet/modules/roles/webstack.pp
class roles::ecommerce_app {
  include profiles::dbserver
  include profiles::webserver
  Class['profiles::dbserver'] -> Class['profiles::webserver']
}

#/etc/puppetpabs/puppet/manifests/site.pp
node 'webapp01.puppetlabs.com' {
  include roles::ecommerce_app
}

In the above example, we've not successfully ordered the MySQL and Apache classes, even though we've ordered the classes that include them.

There are two ways to achieve successful ordering, depending on which version of Puppet you're running.

Class Containment in Puppet Enterprise 

As of Puppet Enterprise 3.2.0, we implemented the contain function.

If we replaced include mysql and include apache, with contain mysql and contain apache, the ordering in the roles::ecommerce_app class from our previous example would extend to the resources in the "contained" classes:

# /etc/puppetlabs/puppet/modules/profiles/manifests/dbserver.pp
class profiles::dbserver {
  contain mysql
}

# /etc/puppetlabs/puppet/modules/profiles/manifests/webserver.pp
class profiles::webserver {
  contain apache
}

# /etc/puppetlabs/puppet/modules/roles/webstack.pp
class roles::ecommerce_app {
  include profiles::dbserver
  include profiles::webserver
  Class['profiles::dbserver'] -> Class['profiles::webserver']
}

#/etc/puppetpabs/puppet/manifests/site.pp
node 'webapp01.puppetlabs.com' {
  include roles::ecommerce_app
}

It's important to be extremely careful with the contain function. For example, the following code:

# testing_multiclass.pp
class a {
  notify { 'foo':}
}

class b {
  notify { 'bar':}
}

class include_first {
  contain a
  contain b
}

class include_second {
  contain a
  contain b
}

include include_first
include include_second

Will generate a catalog successfully:

Notice: Compiled catalog for lolcalhost.local in environment production in 0.10 seconds Notice: foo
Notice: /Stage[main]/A/Notify[foo]/message: defined 'message' as 'foo'
Notice: bar
Notice: /Stage[main]/B/Notify[bar]/message: defined 'message' as 'bar'
Notice: Finished catalog run in 0.20 seconds

But once modified to include ordering:

class a {
  notify { 'foo':}
}

class b {
  notify { 'bar':}
}

class include_first {
  contain a
  contain b
}

class include_second {
  contain a
  contain b
}

include include_first
include include_second

Class['include_first'] -> Class ['include_second']

Catalog compilation will fail:

Error: Could not apply complete catalog: Found 1 dependency cycle:
(Notify[bar] => Class[B] => Class[Include_first] => Class[Include_second] => Class[B] => Notify[bar])

Because we've "contained" class a and class b in multiple locations, and then set order between those locations (classes) containing them, Puppet isn't sure what we're trying to do, and we've got a dependency cycle.

Try Puppet Enterprise

 

This post was originally published on March 21, 2014 and updated for accuracy later.