Looking at the Puppet language in more detail, the most fundamental item in Puppet is a resource. Each resource describes some part of the system and the desired state you wish it to be in. Each resource has a type, which is a definition for the Puppet language of how this particular resource can be configured, which attributes can be set, and what providers can be used. The attributes are what describe the state. So, for a user, this might be a home directory or, for a file, the permissions. Providers are what make the Puppet OS independent since they do the underlying commands be they for creating a user or installing a package.
So, let’s take an example of a company that typically submits build request forms to an environments team to request the configuration for a server:
Table 1.1 – An example build request form
In Table 1.1, the request form, we see groupings of users, groups, and directories, which are all, essentially, types. Each item under them is a resource, and the configuration settings are the attributes.
This request could translate to something like the following:
user { 'exampleapp':
uid => '1234'.
gid => '123'
}
group { 'exampleapp':
Gid => '123'
}
file { '/opt/exampleapp/':
owner => 'exampleapp',
group => 'exampleapp',
mode => 755
}
file { '/etc/exampleapp/':
owner => 'exampleapp',
group => 'exampleapp',
mode => 750
}
The preceding example shows how Puppet translates more directly to user requests and can remain readable without even understanding any of the Puppet language.
What isn’t visible, in this example, is the providers. Puppet has defaults, such as in the preceding example, where the user resource assumes a RedHat host will use the usermod
provider. Instead, if I wished to use LDAP commands for user creation, I would set my provider
attribute to LDAP.
The next important thing to note is that due to the nature of writing Puppet in a stateful way, we are not writing an ordered process that executes line by line but only declaring the state of resources that could be implemented in any order. Therefore, if we have any dependencies, we need to use the relationship
parameter; this describes a before/after relationship, which is exactly as it sounds, or a subscribe/refresh, whereby, for example, updating a configuration file could cause a service to restart. In the previous example, Puppet automatically creates certain dependencies such as ensuring the group is created before the user, so we don’t have to add a relationship parameter. Often, these relationships are seen as one of the most difficult parts of Puppet to adapt to, as many coders are used to writing a process to follow and mistakes can be made. This can cause a cycle of dependencies, whereby a chain of these dependencies cycles round, and there is no way to create a starting resource that isn’t dependent on another.
Evidently, the resources we declare need a structure, and the first step is for this code to be in a file. Puppet calls these manifest files, which have an extension of .pp
. Classes are blocks of Puppet code that give us a way to specifically call sections of code to be run on hosts. Normally, as a good practice, we only have one class in a manifest file. Puppet then uses modules as a way to group these manifests and classes. This grouping is based on the principle that a module should do a single thing well and represent a technical implementation, such as a module configuring the IIS application or configuring postfix as a mail relay. Modules are simply a directory structure storing the manifests, classes, and other Puppet items (which we will cover, in detail, in Chapter 8) and are not a keyword in the language itself. So, ideally, modules should be shareable and reusable for different users and organizations with many taken straight from the Puppet Forge, which is Puppet’s catalog of modules with both commercial and open source offerings.
An example of one common style and practice for modules is to have a manifest file with a single class for the following:
install.pp
(grouping resources related to installing software)
config.pp
(grouping resources related to configuring software)
service.pp
(grouping resources related to running services)
init.pp
(a way of initializing the module and accepting parameters)
At a higher level, we then have roles and profiles, which are used to create the structure of your organization. While modules should be sharable and repeatable installations of technical implementations, such as Oracle or IIS, roles and profiles will only have context within your organization. Roles and profiles are classes used to group modules and selected parameters into logical technical stacks and customer solutions. It is common to make a roles module and a profiles module while keeping together the classes used.
What can be confusing, at this point, is that you can end up with an Oracle role, an Oracle profile, and an Oracle module. So, while the Oracle module configures and installs Oracle with various parameters available to it to customize the installation, the Oracle profile is about how your organization uses this module and what other modules it might add to this technology stack. You might specify that you always use Oracle with a cluster service and, therefore, your Oracle profile contains both an Oracle module and a cluster module. Alternatively, it might pass parameters to the Oracle module within your profile, which set default kernel settings for your organization’s configuration.
You can think of a role as being what the customer actually wants when they submit a build request; they need a particular type of server, be it an Oracle or an IIS server. They don’t care about the underlying implementations – only that it meets their requirements. While the Oracle role will certainly need the Oracle profile, it will expect it to meet the OS security standard and to have any agents or other supporting tools your organization defines. Therefore, a common profile for many organizations is a base OS security standard that ensures every server is compliant and that is part of almost every role.
Figure 1.2 shows an example of what has just been described as an Oracle role class in the roles module, which includes an Oracle profile class and an OS security profile class, both from the profile module. Then, the Oracle profile includes an Oracle module, while the os_security
profile includes the DNS module:
Figure 1.2 – The structure of roles, profiles, and modules
In Chapter 8, we will go into more technical detail, but the key takeaway from this overview is to understand that modules provide sharable and reusable single-use technical installations. In contrast, the roles and profiles pattern provides the context for your organization. Roles are for customers ordering server offerings; they don’t need to understand the technical implementation, only that it meets their business requirement. The profiles in your organization’s technology stack are managed by technical designers and architects, who combine and specify modules according to your organization’s standards and configurations. These roles are responsible for defining how different components are integrated to create the desired technology stack. So while an Oracle module by itself can configure and install Oracle, it is the profile that defines the exact configurations that should be passed to that Oracle module and the other modules it may be dependent on such as having a NetBackup client installed.
With what we have covered in modules, roles, and profiles, going back to Table 1.1, instead, we can have a customer submitting the build request form but not having to specify everything they need; they could simply order an exampleapp
role server.
What we have seen so far is fine when servers meet all the specifications and are standard, but exceptions are commonplace. Hiera is Puppet's data system, and it can be used to pass parameters to the roles and profiles model to handle exceptions. Hiera, as its name suggests, is hierarchical. It defines an ordered lists of data sources to access to find the most relevant setting. These data sources will typically be ordered from the default value for all nodes to a more specific group such as a particular role and specific values for an individual node.
For example, if email servers were disabled by the default OS security profile but were required for exampleapp
, we could have the following YAML file:
exampleapp.yaml
profile::os_security:email_enabled: true
Similarly, if server1
needed a different UID, we could have the following YAML file:
server1.yaml
profile::exampleapp:uid: '1235'
One of the most important points of creating these patterns is to avoid hardcoded values in your modules. By using Hiera, you give yourself a dynamic way to change the values in the future without modifying the code. This could evolve to access the data via a self-service portal – automating away from builds ordered via spreadsheets, emails, and discussions, which would have to be configured by the build teams instead of portals such as VMware vRealize Automation or ServiceNow:
Figure 1.3 – An example portal
In Figure 1.3, an example portal shows how customers can be presented with simplified products. The focus of the Puppet language should be to deliver consistent products to customers and allow customers, architects, and technical staff to focus on what they care about and not have to delve into the technical requirements or coding sections themselves.