Classes and modules are the two main principles/disciplines that Puppet Labs asks any admin to follow while setting up configuration management through Puppet. This provides a structure to configuration management administration.
Class
A class is a name for grouping resources for a common goal or purpose. Why class? Well, it can inherit from other classes and have sub-classes too! Even in Puppet, we have the flexibility of inheritance. Yes, you heard me right — inheritance!
A class is introduced by the class
keyword, and the contents are wrapped in curly brackets. How would you use it to group all similar resources into one category? Take the example of installing the Apache Web service in Ubuntu. We cannot install this service unless a package is installed. So, if we look at the following configuration, it specifies that a service named httpd
(the application name might be different in different Linux distros) must be running, for which a package called apache2
needs to be installed:
class apache { Package { apache2: ensure => installed } Service { httpd: ensure => running, require => Package["apache2"], } }
Also, a problem with just using resources without grouping them is that they get applied to every client. We do not get the flexibility to classify the configuration management on the basis of an operating system, server, package, architecture, etc. Think of it as giving special attention to a specific student, or set of students, in a classroom, when that same attention need not be given to others who have already understood a certain subject.
In the earlier configuration example, how would we handle an Apache service installation for two different GNU/Linux operating systems like Ubuntu and Fedora? Let us look at two classes — apache
and apache_installer
.
The apache class
This class, defined in the file Apache.pp
, says what service needs to be running, and that it needs to depend on the corresponding apache
package. I also use a method to get the value from the sub-class that would be the package name: $title
is what reads the parameter:
class apache { define install_apache { $mypackage = $title Package { $mypackage: ensure => installed } Service { httpd: ensure => running, require => Package[$mypackage], } } }
The apache_installer class
This class (in a file called, Installer.pp
, for instance), depending on the type of OS, would define which package needs to be installed. It would pass the package as a parameter to the earlier method in the parent class.
import "apache.pp" class apache_installer inherits apache { case $operatingsystem { fedora: { apache::install_apache {"httpd":} } CentOS: { apache::install_apache {"httpd":} } ubuntu: { apache::install_apache {"apache2":} } } }
For import, the .pp
extension is not actually required, but I used it because I wanted to differentiate between the apache.pp
file and the apache
class (which is being inherited by the apache_installer
class) inside the apache.pp
file. You can always name your file and class at will. The only requirement is (obviously) the correct file in which the class is defined must be imported, before the class can be inherited.
Now if you look at the two classes above, in Fedora the httpd
service will start after it verifies whether an httpd
package has already been installed (and if not, it will install the package using YUM). In Ubuntu, on the other hand, it will ensure that a package named apache2
is installed. Simple, right?
How do we enable the class? First, import the class (in the above case, installer
or installer.pp
) with the folder path (if you have used a sub-folder to organise class files) into the site.pp
or init.pp
file. Then use the keyword include
to call the class name (in our case, it would be apache_installer
).
The main job is done by the definition inside the first class. We define a method/function to take arguments. Definitions can reuse resources inside a class, but cannot be inherited outside the class, or within another definition of the same class. One of the main reasons why we use a definition is because it has the power to use the values we pass as arguments to the variables $title
and $name
, which a class or any other resource cannot handle.
an automated installer). The preferred option is YUM — it is best supported by Puppet.
We can use classes to even override a certain part of the class, for specific cases. For example, all UNIX-like OSs would have a common setting, but all FreeBSD operating systems could have a certain group overridden, as in the following example:
class unix { file { '/etc/passwd': owner => 'root', group => 'root', mode => 440 } file { '/etc/shadow': owner => 'root', group => 'root', mode => 440 } } class freebsd inherits unix { File['/etc/passwd','/etc/shadow'] [group => wheel] }
Module
A module could be:
- a set of classes, definitions and resources configured for a specific purpose, or
- a repository for configuration files (e.g.,
ntp.conf
), that need to be overwritten on all clients, or - even templates that could be used as a base or format to write a certain configuration. In short, it is a collection of configurations (resources, classes, files, definitions and Templates).
Modules help us organise our configurations better.
The /etc/puppet/manifests/files
folder is the location for all basic configuration files that need to be ported to client machines. The /etc/puppet/modules
folder is the default location for creating modules. This can be changed in the configurations. So the path goes like this:
/etc/puppet/manifests/init.pp
: This is the default, where a module is not explicitly defined./etc/puppet/modules/mymodule/manifests/init.pp
: Here, mymodule is the defined module.
“Manifests” is a must to classify the constituents as a module. This means that the default structure is also read as a module, but it is placed just outside the module path. Modules give more structure than classes, and that is why we include classes inside modules.
The import
keyword that we earlier used to call files from different relative locations inside the manifests folder is the same command used to activate modules as well. So when we say import ssh
, it looks for either:
- a file called
ssh.pp
in/etc/puppet/manifests/
or - a file called
site.pp/init.pp
in/etc/puppet/modules/ssh/manifests/
(ssh
is looked up as the name of a module.)
After importing a module, we can use the include
command to include just one class out of many possibly defined classes in a module. For example, include ssh::server
.
Suppose we want to overwrite the /etc/ssh/sshd_config
file on all machines, how would we go about it? Using modules, this can be done very easily. This is how modules act as a repository too:
file { "/etc/ssh/sshd_config": owner =>" "root", group => "root", mode => "0664", replace => true, source => "puppet:///ssh/sshd.config" }
server_name:///module_name/file_name
is the syntax for the source
keyword. So what’s happening here? A file called /etc/ssh/sshd_config
is created (or replaced, if it already exists) on all clients, with the contents of sshd.config
on the Puppet Server, stored at /etc/puppet/ssh/manifests/file/sshd.config
.
If you recollect, in Part 3 we had classified different resources under various categories. You must have noticed that we have now covered two new resources, one each under the categories Packaging and Services; the resource names are package and service.
I hope you enjoyed exploring these two main disciplines of Puppet, which help to better organise your configuration files. Don’t forget to check up on the official Puppet documentation for further information.