Class Containment in Puppet

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.

What Is Containment?

Containment is the policy of Puppet Labs 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.

As per the docs:

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 Containment?

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 never contain 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 never contain 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 succesful ordering, depending on which version of Puppet you're running.

Class Containment in Puppet Enterprise 3.2.0 (Puppet 3.4.0) and later

If you're using Puppet Enterprise 3.2.0 (or POSS 3.4.0) or later, you're in luck! As of 3.2.0, we've 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.

Class Containment Prior to Puppet Enterprise 3.2.0 (or POSS 3.4.0)

If you're using a version of Puppet Enterprise prior to 3.2.0 (or POSS 3.4.0), you'll need to use the anchor pattern.

# /etc/puppetlabs/puppet/modules/profiles/manifests/dbserver.pp
class profiles::dbserver {
  anchor{'before_mysql:'} -> class{'mysql':} -> anchor{'after_mysql':}
}

# /etc/puppetlabs/puppet/modules/profiles/manifests/webserver.pp
class profiles::webserver {
  anchor{'before_apache:'} -> class{'apache':} -> anchor{'after_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
}

Basically this is the same as contain, but a bit more verbose, in that it requires you to essentially "bookend" classes you'd like to contain using dummy anchor resources.

That's about it for class containment, including both the anchor pattern and the contain function.

Zachary Alex Stern is a support engineer at Puppet Labs.

Learn More

Comments

Zachary Alex Stern

Zachary Alex Stern

There's no denying that I lifted a ton of this from the docs page on containment, found here: http://docs.puppetlabs.com/puppet/3/reference/lang_containment.html

Thanks to our awesome docs team!

LeLutin

LeLutin

Thanks for detailing the two methods and the reason why this is needed.

The anchor pattern may be a bit more verbose but it's still more appealing to my eyes, since it doesn't have the weird corner case that contain() has with ordering.

Xorghan

Xorghan

See http://blog.mayflower.de/4573-The-Puppet-Anchor-Pattern-in-Practice.html for another article on anchors/containment

helindbe

helindbe

It may be worth pointing out that include means "include in catalog" - this helps set the concepts class containment and inclusion apart. Even though we say "class x is included in class y", what we really mean is "class y added x to the catalog (if it was not already there)"

nathan@pupppetlabs.com

nathan@pupppetlabs.com

+1 to helindbe's comment.

Boyan Dimitrov

Boyan Dimitrov

Also note that currently there is a bug in the contain function, which prevents you from using it with fully qualified classes, which is a major showstopper in many use cases. More info here: https://jira.puppetlabs.com/browse/PUP-1597

Levi

Levi

So what's the best way to contain parameterized classes?

Declare them and use chaining arrows against the containing class?

Nick H

Nick H

Levi, according to the language docs linked in the post, using the resource-like declaration still doesn't contain that class, so you have to follow your "class { 'mco' : ... }" with another line to say "contain mco".

Leave a comment