Chapter 3. Coding Practices

In this chapter, we look at best practices related to using the Puppet language. This chapter focuses predominantly on Puppet’s conditional logic, operators, function calls, and data structures. We concentrate on resource types and classes at a high level so that we can delve deeper on those subjects in Chapter 4 when we discuss module design, and Chapter 5 when we explore best practices relating to a number of common resource types, and Chapter 7 when we look at code organization and intermodule relationships.

The Style Guide

We advise you to review the Puppet language style guide, which covers recommended usage and what validation and testing tools to expect. We do not repeat the style guide in this chapter; instead, we expand upon the information Puppet provides in the guide, emphasizing a few key and often overlooked aspects of the guide.

The Puppet style guide focuses on keeping code clear and easy to understand, as well as maintaining a clean revision history. A major benefit of the official style guide is that development and testing tools automatically identify and correct violations of the guide.

The Puppet style guide is now updated for each version of Puppet. Always refer to the version you are coding for, like so:

Be aware that the style guide often contains recommendations made to ensure backward compatibility with older Puppet releases. This is useful for shared modules used by a wide audience. If you’re planning to release code that might be used with older versions of Puppet, the style guide will keep you safe. Otherwise, it’s usually best to code to current best practices, and leave those deprecations behind.

Tip

The style guide once recommended use of the params.pp pattern for compatibility with 3.x releases. Use it if you still have Puppet 3 deployed, but otherwise leave the past behind. Puppet 4, 5, and 6 all support the significantly more powerful data in modules pattern.

Having a set style guide is invaluable for peer review. Consistent style helps reviewers focus on the changes in your code, without struggling to read past differences in style. Simple rules such as indentation can make errors stand out. Correct use of white space, variable assignment, and interpolation make the code easier to follow. If you choose to not follow the Puppet style guide, we recommend documenting and publishing an internal style guide for your site. Your internal guide could be based on the Puppet standard, with your own internal adjustments documented.

When working in a team that uses its own style guide for other projects, it can be a good idea to adapt that style for use in Puppet for team or project consistency. One consistent imperfect style is much better than multiple coding styles, though this doesn’t mean adopting bad practices in the name of consistency.

Install puppet-lint and use it in a pre-commit hook to encourage conformity to the style guide. We discuss puppet-lint more extensively in “Testing”.

Coding Principles

Principles for development improve the quality of your code. Books such as Practical Object-Oriented Design in Ruby by Sandi Metz and Clean Code by Robert C. Martin go into great depth on coding practices and do so in a way that is applicable beyond the scope of the language examples used in the book.

Many object-oriented development principles apply to Puppet, though some principles are procedural in nature and can thus be counterproductive due to the nature of Puppet’s declarative language and idempotent design. This section introduces declarative and object-focused coding principles that are referenced throughout the book.

KISS: Keep It Simple

Good Puppet code is not clever. Good code is straightforward, obvious, and easy to read. There is no twist, no mystery. Although there are many neat tricks that can reduce the number of characters used, you and others will spend a lot more time reading your code than you did writing it. Readable code is less fragile, easier to debug, and easier to extend. Code reduction is most beneficial when it eliminates potential bugs and improves readability.

Clever code tends to make sense when written, but often becomes confusing when read months later. Clever code often hides subtle bugs, and can be difficult to extend or refactor. In the worst cases, it solves unexpected problems in nonobvious ways, and bites anyone who attempts to refactor it.

If a trick improves the readability and clarity of your code, by all means use it. If the neat trick has obtuse inner workings or makes the person next to you scratch their head, consider using a more readable approach that’s easier to understand.

Tip

Code that’s obvious to you today might not be so to the person who must read or update it. Even you likely won’t recall your own intent after enough months or years have passed.

Even documented design patterns and techniques can be confusing to those new to Puppet or that particular technique. The benefit of documented tricks is that they tend to be well understood and well explained. Example 3-1 demonstrates needless complexity.

Example 3-1. Violating the KISS principle using create_resources()
$files = {
  '/tmp/foo' => {
    'content' => 'foo',
  },
  '/tmp/bar' => {
    'content' => 'bar',
  }
  '/tmp/baz' => {
    'ensure' => 'link',
    'target' => '/tmp/foo',
  }
}

$defaults = {
  ensure => 'file',
  mode   => '0644',
}

create_resources('file', $files, $defaults)

Using create_resources to iterate over a hash is a well-known pattern and is a best practice when iterating over a large number of items sourced from an external data source. But the implementation in Example 3-1 is both more lines of code—and less readable—than simple file resource declarations. See “DRY: Don’t Repeat Yourself” later in this chapter for more examples.

The Single Responsibility Principle

Resources, defined types, and function calls in Puppet should have one responsibility, and no more than one responsibility. Examples of statements that violate the single responsibility principle:

  • This module installs Java and Tomcat.

  • This conditional sets the ensure state and determines the correct version.

  • This resource downloads and extracts a tarball.

  • This class configures NGINX and installs a baseline environment.

  • This module encrypts data and synchronizes the system clock.

A module built with this description attempts to solve multiple problems in a single place, thus embedding limitations and contraints on reuse. The single responsibility principle suggests that code built around singular responsibilities will be better designed and reusable. We can achieve a key difference simply by defining responsibilities without using the word “and,” as shown here:

  • This class manages Apache’s packages.

  • This select statement retrieves platform-specific defaults.

  • This function call validates URLs.

  • This resource downloads a file.

  • This module installs Tomcat.

  • This role builds our three-tier application stack on a single node.

A Puppet module that follows the single responsibility principle is easy to reuse, maintain, and extend. It doesn’t include its own copy of Java; it allows you to use a class designed specifically for that need. The class’ structure is broken down into child classes, each of which are named appropriately for their single purpose. This allows the reader to easily grasp the flow of the module and its relationships.

When you apply the single responsibility principle, modular, extensible, and reusable code is created. It helps ensure that side effects are minimized, and simplifies debugging. It’s easy to create unit and acceptance tests for Puppet modules built around the principle. The behavior of each bit of functionality will be uniform, easy to document, and easy to test.

Separation of Concerns

The separation of concerns (SoC) principle makes it possible to create loosely integrated, modular code. It is is strongly related to interface-driven design and the single responsibility principle.

Many of the concepts discussed in Chapter 2 service the SoC:

  • Modules implement a single application or component.

  • Data sourced from Hiera describes the implementation.

  • Roles and profiles incorporate business and service logic for complete stack implementations.

SoC requires thought to where a particular bit of code or data belongs. It means asking questions such as the following:

  • Is this business logic or application data?

  • Should this be specified in data or static in this profile?

  • Is this configuration part of the application logic or profile-specific logic? Example: Should a VMWare-specific time setting be in the NTP or the VMWare module?

Tip
In the last scenario, the NTP configuration change is applicable only to VMware nodes. Therefore, the correct choice is to create a profile that includes both modules and implements this one change common to, but outside of, both of them.

SoC can be difficult to maintain. Referencing an existing variable from another module seems like a quick and easy way to access data. It avoids duplicating data and avoids the need to reconsider structure. Unfortunately, this is always a trap. Violating another module’s interface makes your code tightly interwoven with that module, making any change a risk for both modules. It is thus difficult to debug and maintain, constrains improvements, and hampers refactoring.

Warning
Failing to keep modules discrete creates situations where it’s impossible to test a module without deploying the entire codebase.

Modular Puppet code minimizes dependencies, allowing quick instantiations of the module to validate the module’s functionality. With modular code, you can test and debug one component at a time. With an interwoven codebase, understanding and troubleshooting any single component of the system involves troubleshooting the entire application stack.

Tip
Properly implementing SoC manifests the design goal of low coupling and high cohension: the module resources are tightly focused on its core concerns while loosely bound to other modules.

Interface-Driven Design

Puppet modules should interact with one another using well-defined interfaces. In most cases, this means that you interact with classes via class parameters and class relationships rather than using fully qualified variables and direct resource relationships.

Because Puppet is a declarative language and does not offer getter or setter methods, interface-driven design tends to enforce a structure to the way data flows through your site.

  • Data originates from sources such as Hiera, ENCs, and your site.pp manifest.

  • The data is consumed by roles and profiles.

  • component modules. This all happens by having the component modules receive parameters from the roles and profiles.

With this approach, the business logic instructs the general-purpose application modules how to build the site-specific requirements.

Using class parameters as your primary interface providers troubleshooting benefits. Information about the classes and class parameters applied to a node are sent to that node as part of its catalog. Referring to the built catalog, you can isolate and debug a single module without worrying about how its parameters are being generated. You can also use the parameters to better understand how data flows through your manifests.

Example 3-2 presents code that makes two bad choices that violate the principles of interface-driven design.

Example 3-2. vhost template violating the principle of interface-driven design
class myapp {
  include apache
  $vhost_dir = $apache::vhost_dir

  file { "${vhost_dir}/myapp.conf":
    ensure  => 'file',
    content => epp('myapp/vhost.conf.erb', {
                  hostname => 'myapp.example.com',
                  port     => '443',
                  docroot  => '/srv/my_app/root',
                })
  }
}

This module does two things wrong:

  • It violates the module interface to retrieve the configuration directory.

  • It writes a file into the module’s managed configuration directory.

Example 3-3 provides the same delivery as Example 3-2 except that it uses the module’s public interface. This ensures that the usage will work, regardless of whether the Apache module uses a different variable for the configuration directory in the future, or purges unmanaged content from its directory.

Example 3-3. Interface-driven virtual host creation virtual host creation
class myapp {
  include apache

  apache::vhost { 'myapp.example.com':
    ensure  => 'present',
    port    => '443',
    docroot => '/srv/my_app/root',
  }
}

In Example 3-2, the code reaches into the Apache module and grabs the contents of an internal variable. If this is not a documented interface of the class, there is no guarantee that the variable or its contents will be available or consistent between releases.

Rather than querying the Apache module to determine the directory and then inserting custom content there, in Example 3-3 we utilize a defined type provided by the Apache module that creates the vhost for us. This approach uses a stable interface provided by the Apache module while retaining our ability to control the virtual host definition our application requires. Because apache::vhost is provided by the Apache module, it has safe access to Apache internals without violating the principles of interface-driven design.

Although versions up through Puppet 6 don’t prevent you from directly accesssing variables from other modules, Puppet has indicated that it is considered deprecated behavior and might be removed in future versions of Puppet. The best reason to avoid direct access of variables internal to another module is that it will improve the quality of your code, facilitate debugging and analysis, and avoid headaches when the module is refactored.

DRY: Don’t Repeat Yourself

The DRY principle suggests that if you write the same code or data more than once, it could be abstracted into a class, defined type, or function call.

Puppet provides the ability to extend the language with Ruby facts, functions, custom Hiera backends, data providers for environments, and modules. Puppet 4 and higher support facts and functions written entirely in the Puppet DSL to reduce repetition in your code.

The best application of the DRY principle can be subjective and might differ from one use case to another. If reducing repetition in your code makes it unreadable, it reduces the code quality rather than enhance it. We advise preferring readability over DRY for any case in which the principles conflict.

Consider Example 3-4:

Example 3-4. Repetitive resource declarations
file { '/etc/puppet/puppet.conf':
  ensure => 'file',
  content => template('puppet/puppet.conf.erb'),
  group  => 'root',
  mode   => '0444',
  owner  => 'root',
}

file { '/etc/puppet/auth.conf':
  ensure => 'file',
  content => template('puppet/auth.conf.erb'),
  group  => 'root',
  mode   => '0444',
  owner  => 'root',
}

file { '/etc/puppet/routes.yaml':
  ensure => 'file',
  content => template('puppet/routes.yaml.erb'),
  group  => 'root',
  mode   => '0444',
  owner  => 'root',
}

You could make this example DRY by using default attribute values, as shown in Example 3-5.

Example 3-5. Resource declarations with attribute defaults
file {
  default:
    ensure => 'file',
    owner  => 'root',
    mode   => '0444',
    group  => 'root',
  ;

  '/etc/puppetlabs/puppet/puppet.conf':
    content => template('puppet/puppet.conf.erb'),
    group   => 'wheel',
  ;

  '/etc/puppetlabs/puppet/auth.conf':
    content => template('puppet/auth.conf.erb'),
    mode    => '0400',
  ;

  '/etc/puppetlabs/puppet/routes.yaml':
    content => template('puppet/routes.yaml.erb'),
  ;
}

As shown in this example, readability is improved by setting resource default values. The code is condensed, and exceptions stand out clearly to the reader.

Caution

Default attribute values in a resource expression are more readable and less prone to breakage than default resource definitions (e.g., File), which can affect resources off the page. Default resource definitions can have wide-randing but inconsisent impact and produce surprising behavior when minor changes to the catalog affect class load order.

You could further simplify this example by using a defined type, as demonstrated in Example 3-6.

Example 3-6. Resource declarations with a defined type
$files = [
  '/etc/puppet/puppet.conf',
  '/etc/puppet/auth.conf',
  '/etc/puppet/routes.yaml',
]

mymodule::myfile { $files: }

Although this approach uses fewer characters, the implementation is hidden from the viewer. The resulting behavior relies on the implementation of the Mymodule::Myfile defined type, which could change outside of the scope of this example. This highlights the inherent problem with this approach: it hides implementation details and forces cross-reference when extending or debugging this code.

You can accomplish the same thing using an each() loop., as shown in Example 3-7.

Example 3-7. Resource declarations with an each() loop
include 'stdlib'

$files = [
  '/etc/puppet/puppet.conf',
  '/etc/puppet/auth.conf',
  '/etc/puppet/routes.yaml',
]

$files.each |$file| {
  file { $file:
    ensure  => 'file',
    content => template("puppet/${file}.erb"),
    group   => 'root',
    mode    => '0444',
    owner   => 'root',
  }
}

This approach is preferable to a defined type by keeping the resource declaration logic and iterator in the same file, and often on the same page of code. The code is better contextualized for the reader, and there is no latent dependency on external code.

In practice, Example 3-5 is the most readable approach, despite being less DRY than Example 3-6 or Example 3-7. This is a case for which simplicity wins out over other considerations.

There are a few cases in which very DRY code can be produced using array resource declaration and depending on automatic relationships:

$directories = [
  '/tmp',
  '/tmp/example1',
  '/tmp/example2',
  '/tmp/example1/foo',
  '/tmp/example1/bar',
]

file { $directories:
  ensure => 'directory',
  mode   => '0755',
}

In this example, each of the directories is created by passing an array of directory paths to the file resource. This simple example depends on autorequire logic in the File resource to automatically create relationships between the higher- and lower-level directories. Without this automatic relationship, you’d need to break these out to create the relationships one by one, as shown here:

file { '/tmp':
  ensure => 'directory',
  mode   => '0755',
}

file { '/tmp/example1':
  ensure  => 'directory',
  mode    => '0755',
  require => File['/tmp'], # redundant, is automatic dependency
}

file { '/tmp/example1/foo':
  ensure  => 'directory',
  mode    => '0755',
  require => File['/tmp/example1'], # redundant also
}

The bolded lines in the preceding example are unnecessary given that the File resource automatically creates relationships with directories in parent paths.

Don’t Reinvent the Wheel

Attempts to reinvent existing tools using the Puppet DSL are the most common cause of fragile, code-heavy Puppet manifests. For example, it can take a lot of conditional logic and exec resources to download, cache, and extract a .tar file from a web server. You can perform the same process idempotently with a single archive resource.

Puppet is not a software packaging tool; there are more than three dozen package providers purpose-built for this need. The package resource makes use of these providers to manage packages. There can be odd situations for which using Puppet to build packages might seem unavoidable, but the quality of code will suffer, and the complexity of the implementation will increase dramatically.

YAGNI: You Ain’t Gonna Need It (or You’re Not Google)

Avoid taking a small requirement and setting out to build a solution (that you think) Google would be proud of. We have seen far too many simple requests turned into a multiweek deliverable when the need didn’t require it. This often results in overly abstracted, difficult-to-decipher implementations that provided the same features that existing functionality like a simple key/value store could provide (unless you count lost hours of effort).

Is the requirement for highly performant code or is it for a well-maintained solution delivered quickly? Most companies without Google’s scale would rather throw money at commonly available compute resources than at difficult-to-find, comparatively expensive infrastructure engineers.

We advise being data driven in your selection of efforts. Document the needs that are known at the start. Observe that certain future needs might not be met with a selected existing functionality. But don’t go coding The Best New Key Query Store Evah until data shows those conditions are necessary. This avoids unnecessary work, known widely as You Ain’t Gonna Need It, and colloquially as You’re Not Google.

Code Practices

This section covers best practices relating to code patterns that improve readability with the Puppet language. These are all subjective, but it’s usually quite easy to see the difference when you’re comparing two different implementations.

Balance of Resources Versus Logic

Puppet code contains instructions on how to build a catalog of resources. Those resources describe the state that the resource providers on the node will ensure.

Conditional logic by definition is not idempotent. It’s a procedural construct that guarantees that given the same input, it will return the same output. Although Puppet variables, selectors, and conditionals are a core part of the Puppet language, their ultimate purpose is to declare resources to be added to the node’s catalog. Logic implemented in the Puppet language without resources can never be idempotent; only the resources it declares can function idempotently. Puppet works best when decisions about how to implement the declared state are left to the resource providers.

When you find a module heavy with conditional logic, case statements, and variable handling code, take a moment to reassess your approach. Is there a way to refactor this code to be more declarative? To illustrate this further, let’s take a look at Example 3-8.

Example 3-8. Resources versus logic
# BAD DESIGN: conditionally create resources specific to the data
if( $users['jill']['shell'] == '/bin/bash' ) {
  user { 'jill':
    ensure => 'present',
    shell  => '/bin/bash',
  }
}

# GOOD DESIGN: let the data inform the user provider what to implement
user { 'jill':
  ensure => 'present',
  shell  => $users['jill']['shell'],
}

Catalog building works best when the bulk of your manifests comprise resource declarations rather than logic. Declare the desired state, and leave it up to the resource provider to choose when and how to act.

Balance of Code Versus Data

Puppet is often described as infrastructure as code because it creates infrastructure with code based on the data provided.

Puppet code is reusable when the code does not contain data values, but instead implements application or business logic based on the data provided. A Puppet module should ideally contain little to no data within the module, instead acting upon data provided by Hiera.

To understand this point better, let’s look at Example 3-9, which contains a considerable amount of data.

Example 3-9. Data values in code
case $facts['os']['family'] {
  'RedHat': {
    package { 'apache24':
      provider       => 'yum',
      vendor_options => '--enablerepo=epel',
    }
  }
  'Debian': {
    package { 'apache2':
      provider => 'apt',
    }
  }
}

This example hardcodes the name of the package, the name of the repository, and the provider into the module. All of these are subject to change. In Red Hat Enterprise Linux (RHEL) version 8 and above, the provider will be DNF, the default Apache version will be 2.6...you get the idea. But most important of all, none of these data values describe what is being done. Rather, they provide specifics about how to do it, as shown in Example 3-10.

Example 3-10. Code utilizing supplied data
$pkginfo = lookup("myapp::packages::${facts['os']['family']}")

package { $pkginfo['name']:
  ensure         => $pkginfo['should_update'],
  provider       => $pkginfo['provider'],
  vendor_options => $pkginfo['vendor_options'],
}

Example 3-10 is much easier to read, and uses values supplied by Hiera for the packages and providers appropriate for each OS.

Conditional Logic

As shown in Example 3-9, you should avoid placing resources inside of conditional logic whenever possible. Doing so makes the generated catalog subject to change and removes important information from the node’s catalog and reports. Let’s look at this in Example 3-11.

Example 3-11. Resources embedded in conditional logic
case $facts['os']['family'] {
  'RedHat': {
    package { 'httpd':
      ensure => 'installed',
    }
  }
  'Debian': {
    package { 'apache2':
      ensure => 'installed',
    }
  }
}

When comparing the catalogs of two otherwise identical nodes, Package['httpd'] and Package['apache2'] would appear to be different but implement the same functionality for two different platforms. You can achieve the same net result with a consistent resource name and acquiring OS–specific values from data, as demonstrated in Example 3-12.

Example 3-12. Use data to simplify resource management
package { 'apache':
  ensure   => 'installed',
  name     => $pkginfo['name'],
  provider => $pkginfo['provider'],
}

When reading the code, comparing the catalog or reports of catalog application, these resources will clearly be identical. No lines of code are wasted on OS-specific details better listed in the data.

Example 3-12 is significantly more DRY than Example 3-11 and better communicates its intent.

This concept extends to resources that might be optional within a catalog. Let’s look at Example 3-13, which conditionally adds a resource an autosign.conf file:

Example 3-13. Conditionally adding a resource to a catalog
  if $autosign == true {
    file { '/etc/puppet/autosign.conf':
      ensure  => 'present',
      content => template('puppetserver/autosign.conf.erb'),
    }
  }

This code has two problems:

  • It won’t remove the file if the value changes from true to false.

  • It causes the File['autosign.conf'] resource to wink in and out of existence in the catalog.

A significantly more stable implementation is provided in Example 3-14.

Example 3-14. Conditionally managing resource state
  $autosign_ensure = bool2str($autosign, 'present', 'absent')
  file { '/etc/puppet/autosign.conf':
    ensure  => $autosign_ensure,
    content => template('puppetmaster/autosign.conf.erb'),
  }

Example 3-14 declares what the result should be in either case, rather than the conditional action shown in Example 3-13. In doing so, it ensures the proper state by changing the file declaration to be absent and having the file be removed.

There are some cases for which it makes absolute sense to wrap a resource in conditional logic, such as the following:

if $pkginfo['uses_sites_available'] {
  file { "/etc/apache2/sites-enabled/${title}.conf":
    ensure => 'link',
    target => "../sites-available/${title}.conf",
  }
}

This resource would be nonsensical on a Red Hat–based system because Red Hat does not manage Virtual Hosts using the sites-available/ and sites-enabled/ directories. In this case, conditionally adding the resource to the catalog is the correct approach because that directory structure won’t exist if the sites-available configuration style is not in use.

Iteration

Iteration is the act of applying the same process to multiple elements in a set. Puppet 3 had limited support for iteration. Puppet 4 and newer introduced iteration functions that are a great deal more readable and flexible than previous solutions.

Iteration with resources

Until Puppet 4, only two features allowed iteration over multiple resources. We cover these because they remain available in newer versions of Puppet.

Resources can be declared with an array of titles

If you declare a resource with more than one title, it will process each one as a distinct resource declaration. This works well only when the attributes are consistent in each resource, although it is most often used to call a declared type that acts on each value.

create_resources() creates a named resource for every entry in the hash

Call the create_resources() function with the name of a resource and a hash of resource attributes. It will create a resource declaration of the named resource for each entry in the hash.

For cases in which you want to declare a bunch of resources with a hash, create_resources() interates over an easy-to-maintain data structure, as illustrated in the following example:

$services = {
  'puppet'       => { 'ensure' => 'running', },
  'puppetserver' => { 'ensure' => 'running', },
  'httpd'        => { 'ensure' => 'stopped', },
}

create_resources('service', $services)

In situations that require something more complex than mapping data to resource attributes, you can call a defined type via create_resources() to process or evaluate input. Example 3-15 demonstrates an implementation to automatically adjust the enabled attribute of a resource based on the ensure parameter.

Example 3-15. Iteration using a defined type
$services = {
  'puppet'       => { 'ensure' => 'running', },
  'puppetserver' => { 'ensure' => 'running', },
  'httpd'        => { 'ensure' => 'stopped', },
}

create_resources('ensure_service', $services)

define ensure_service (
  $ensure => undef,
) {
  $enabled = $ensure ? {
    'running' => true,
    default   => false,
  }

  service { $title:
    ensure  => $ensure,
    enabled => $enabled,
  }
}

Iteration with functions

Functions that iterate across arrays and hashes of data provide a level of flexibility not available prior to Puppet 4.

Puppet 4 introduced a number of block iterators. The functions filter, map, reduce, scanf, and slice are most useful for data transformation. Each of these functions takes one set of data and processes it to create another form. For example, filter can be used to produce an array or hash containing only values that meet certain criteria.

The example that follows filters a hash of users to extract a smaller hash containing only users who are members of the admin group.

$admins = $users.filter |$username, $values| {
  $values['groups'] =~ /admin/
}

For each user in $users, a test of the groups value is performed. A positive match causes the entire user entry to be added to the $admins hash.

In contrast, the each() function passes each value to a block of code for processing without returning any value to the calling scope. The following is a very basic reimplementation of create_resources():

$services.each |$name, $values| {
  service { $name:
    ensure  => $values['ensure'],
    enabled => $values['enabled']
  }
}

Generating Lists

There are a number of cases for which you might want to dynamically generate a list of items. This can happen for benchmarking purposes, or to supply a range of times for cron jobs.

The range() function from puppetlabs/stdlib accepts a beginning value, an end value, and an optional interval value, and then returns an array of values. This makes it useful for specifying intervals, such as with cron:

cron { 'every_5_minutes':
  command => something to be done every 5 minutes
  minute  => range(0, 55, 5)
}

Although the range() function can provide some limited iteration of numbers within a string, it’s more reliable to generate an array of integers and use prefix() and suffix() to create names. prefix() and suffix() each accept an array and a string and return an array with the prefix or suffix applied to each element, as shown in this example:

Example 3-16. Iterating over a flat hash using each
$range    = range('0','2')
$prefixed = prefix($range, '/tmp/test')
$complete = suffix($prefixed, '.txt')

You can see the output of this code by testing with puppet apply. This example uses the less readable but concise function chaining:

$ puppet apply -e "notice( range('0','2').prefix('/tmp/test').suffix('.txt') )"
Notice: Scope(Class[main]): [/tmp/test0.txt, /tmp/test1.txt, /tmp/test2.txt]

Variables

This section covers a number of best practices relating to the use of variables in the Puppet language.

Variable Naming

Correct naming of variables is very important for both usability and compatibility.

All supported versions of Puppet (4+) have restrictions on variable naming, the most major of which is that you can no longer start variable names with capital letters.

You must follow these guidelines:

  • Begin variable names with an underscore or lowercase letter.

  • Subsequent characters may be uppercase or lowercase letters, numbers, or underscores.

Most identifiers in Puppet loosely follow Ruby symbol naming restrictions; the identifier cannot begin with a number and cannot contain dashes. Class names are even more strict because they normalize to lowercase (flattening capitals), which will impair readability. We advise erring on the side of using more restrictive conventions and favoring descriptive variable names over terse variable names.

Referencing Variables

We recommend the following guidelines when referencing a variable:

  • Avoid variable inheritance.

  • Local variables should be unqualified.

  • Global variables should be fully qualified.

  • Reference facts using the $facts[] array.

  • Avoid referencing undefined variables.

Variable should be fully qualified

Local variables should be unqualified, global variables should be fully qualified, and you should avoid fully qualified out-of-scope variable references whenever possible.

Fully qualifying variable names accomplishes two goals:

  • Clarify your intent

  • Disambiguate local and global variables

When a variable is fully qualified, it becomes clear that your module is attempting to consume a top-level variable and eliminates the possibility that you simply forgot to define that variable or are attempting to inherit a variable from a higher scope. This disambiguation will be important when you revisit your code in the future, either to extend the code or debug a problem.

Warning

Many validation tools assume that unqualified variables are local and will throw a warning if the variable is not defined in scope. puppet-lint allows you to disable this behavior; however, we recommend against doing so because it’s an essential tool for identifying subtle bugs.

Although you can fully qualify references to local variables, using unqualified names makes it clear at a glance that the variable is local and has been defined in the current scope. This hint again is used by the validators.

Avoid creating interclass variable references using fully qualified variable names. Such references are a useful stop-gap measure when upgrading code that relies on variable inheritance; however, it usually violates the SoC principle. A major issue with interclass variable access is that there’s no way to determine from the referenced class that such a reference exists. As a result, any code change in one class could break the reference. Instead, consider parameterizing your classes, using class parameters to pass data from one module to another, as demonstrated in Example 3-17. A side benefit of this approach is that it tends to reduce and eliminate circular dependencies.

Example 3-17. Passing data using fully qualified variables
class parent {
  $foo = 'alpha'
}

class parent::child {
  $foo = $parent::foo
  alert($foo) #Prints 'alpha'
}

In this situation, someone refactoring parent wouldn’t necessarily be aware that variable $foo is being used elsewhere. In Example 3-18, the data usage is explicit, allowing either or both modules to be refactored without affecting the other.

Example 3-18. Passing variables using class parameters
class parent {
  $new_name = 'alpha'

  class { 'child':
    foo => $new_name,
  }
}

class parent::child (String $foo) {
  notice($foo) # logs 'alpha'
}

Example 3-18 is more verbose than Example 3-17, but is designed in such a way that variable usage is clearly documented in code. This increase in verbosity improves the quality of the code and reduces the likelihood of breakage as the code evolves.

Avoid referencing out-of-scope variables. Instead, pass variables into classes using class parameters, as shown in the code that follows. This might seem like an arbitrary guideline at first, but it helps to control a flow of data through your code and avoids surprises when refactoring code.

class profiles::variable_scope {
  $foo = 'alpha'

  class { 'first':
    foo => $foo,
  }

  class { 'unrelated':
    foo => $foo,
  }
}

class first (String $foo) {
  notice($foo) # logs 'alpha'
}

class unrelated (String $foo) {
  notice($foo) # logs 'alpha'
}

Fully qualified variables make it easy for minor code changes to break things in different codebases, whereas use of parameters for passing values enforces the loosely coupled, SoC approach to data handling.

It used to be necessary to reference fully qualified variables to work around limitations in Puppet. The deprecated params.pp pattern discussed in “The params.pp Pattern” is an example of such a workaround. But all supported versions of Puppet provide the features necessary to keep the boundary intact.

Other Variable Use Cases

Variables can be tremendously powerful for documenting your code. Well-named variables can create meaning and context for complex data interactions. Further, they can tremendously simplify understanding when complex quoting and escaping are used.

Avoid anonymous regular expressions

A non-Puppet example: the purpose of the following regular expression might not be immediately clear:

grep -o '`d{1,3}.d{1,3}.d{1,3}.d{1,3}`' logfile

The reader is forced to decode the regular expression to understand the usage. Although this is not difficult in this case, it slows down or prevents comprehension depending on the reader’s knowledge of regular expressions, or regexes.

Prefer named regular expressions

The purpose of the regular expression can be made apparent by assigning it to a well-named variable before using it:

IPV4_ADDR_REGEX='d{1,3}.d{1,3}.d{1,3}.d{1,3}'

grep -o $IPV4_ADDR_REGEX logfile

This usage makes the same code readable to someone who doesn’t understand regular expression syntax and also speeds reading for someone who can parse it but doesn’t need to.

Variables containing regex patterns can be invaluable when you have complex quoting requirements, such as working with directory paths containing spaces on Windows hosts. Consider the following example:

$sevenzip    = '"C:Program Files7-Zip7z.exe"'
$archive     = '"C:	emp spacearchive.zip"'
$output_path = '"C:	emp spacedeploy"'

exec { 'extract_archive.zip':
  command => "${sevenzip} x ${archive} -o${output_path} -y"
  creates => $output_path,
}

This example is dramatically simplified by declaring our paths as single-quoted strings and interpolating them together in a double-quoted string. Single-quoted strings allow you to embed quotes in the paths containing spaces and avoid the need to double escape the directory-delimiting backslashes.

Trusted Variables

Puppet 3.4 introduced the $trusted[] data hash and Puppet 3.5 introduced the $facts[] data hash. These hashes are enabled by default in all supported versions of Puppet.

The values in the $trusted hash are provided by or validated by the Puppet server/master process. It doesn’t matter what version of Puppet agent is running on the node because these values are set by the master only for use when building the catalog. This means that you can rely on the $trusted[] hash to be available. Using it helps disambiguate server-validated facts, and can prevent some injection attacks against exported resource collectors.

If you are supporting a mixture of Puppet 3 and modern Puppet agents, it is crucial to get the Puppet 3 nodes up to the final version of Puppet 3 (3.8.7) so that you’ll have consistent access to the $facts hash.

Tip

Better yet, get them up to at least Puppet 4.10—the oldest supported version of Puppet at the time this book was released.

Remember that a compromised node can arbitrarily define facts. Data in the trusted[] hash is guaranteed to be supplied or validated by the Puppet server.

If you are using global variables for anything security sensitive, declare them unconditionally in a global manifest or your ENC to avoid the risk of abuse via client-side facts.

Order of Assignment for Top-Level Variables

Global variables can be defined from a number of different sources, including your ENC, your site-wide manifests, by the Puppet interpreter, by the Puppet Master, and via facts supplied by the client.

In some cases, the variables defined by different sources are reserved; for example, interpreter-defined variables such as $name, $title, and $module_name. In other cases, more authoritative data sources will override less authoritative data sources.

Top-level or un-namespaced variables are declared by the following sources:

  • Facts provided by the node

  • Parameters supplied by the ENC

  • Global variables in top-level manifests

Assignment with Selectors

You can use a selector to improve handling of data provided. The following example maps multiple data formats to acceptable values:

$file_ensure = $ensure ? {
  'false' => 'absent',    # String value true
  false   => 'absent',    # Boolean value true
  default => 'file',  # anything else
}

file { 'somefile':
  ensure => $file_ensure,
  ...
}

This example correctly evaluates either the String 'false' or the Boolean value false to remove the file. This can be a very effective technique when upgrading older modules to use data types, where you cannot immediately update all uses of the module to pass in the correct type.

Attribute Values Chosen by Conditional Evaluation

Although you can use selectors in resource attribute values, doing so produces obtuse code. As you can see in the following example, you are forced to keep reconsidering what is being done at multiple levels:

  service { example:
    ensure => $ensure ? {
      'absent' => 'stopped',
      default  => 'running',
    },
    enabled => $ensure ? {
      'absent' => false,
      default  => true,
    },
  }

This same example is significantly improved by using selectors to declare variables used for the attributes in the resource, as shown in the preceding code.

  $service_enable = $ensure ? {
    absent  => false,
    default => true,
  }

  $service_ensure = $ensure ?
    absent  => 'stopped',
    default => 'running',
  }

  service { example:
    ensure => $service_ensure,
    enable => $service_enable,
  }

Another easy-to-read implementation groups the value assignments using a case statement:

  case $ensure {
    'present': {
      $service_ensure = 'running'
      $service_enable = true
    }
    'absent': {
      $service_ensure = 'stopped'
      $service_enable = false
    }
  }

  service { example:
    ensure  => $service_ensure,
    enable => $service_enable,
  }

Variable Inheritance

Variable inheritance has been effectively removed from Puppet. Each version of Puppet since 3.0 has limited variable inheritance even further. Since Puppet 4, nearly all variable inheritence was removed in favor of fully qualified variable names.

Puppet still permits a variable inheritence only when a class inherits from another class, in which case the parent class variables are inherited into the child class. You can use this to override specific values within a specific implementation.

class auto {
  $type = 'vehicle'
  $wheels = 4

  notice($type)   # logs 'vehicle'
  notice($wheels) # logs 4
}

class motorcycle inherits auto {
  $wheels = 2

  notice($type)   # logs 'vehicle'
  notice($wheels) # logs 2
}

You should avoid class inheritance except in this specific use case for overriding a value in the child class. We explore this in greater depth in Chapter 4.

Strict Variables

With the strict_variables configuration setting enabled, referencing an undefined variable throws an error. Enabling this setting can help you catch all kinds of bugs and typos. Example 3-19 assumes that the unknown variable should resolve to an undefined value and so ouputs nothing. Depending on how that value is used in the code, this can lead to unintended consequences.

Example 3-19. Undefined variables silently fai...succeed!
$ puppet apply -e 'notice($unknown_var)'
Notice: Scope(Class[main]):
Notice: Applied catalog in 0.03 seconds

In the early days of Puppet, this behavior was commonly used to test for the existence of a fact or variable. While that was convenient, it also made typos prevalent and almost impossible to catch. These days you should use defined($variable) to check for a variable’s existence.

When strict mode is enabled in Example 3-20, it immediately throws an error that includes the exact location of the invalid variable:

Example 3-20. Enabling strict variables catches typos
$ puppet apply --strict_variables -e 'notice($unknown_var)'
Error: Evaluation Error: Unknown variable: 'unknown_var'.  at line 1:8

We encourage you to test modules with this setting enabled. If you happen to encounter a public module that fails with strict_variables enabled, submit a fix to the module author.

Function Calls

Puppet’s rich assortment of built-in and stdlib function calls are one of the more frequently overlooked features of Puppet. We recommend that you read through both the list of built-in functions and the list of puppetlabs/stdlib function calls once or twice to familiarize yourself with what’s already available. Very often there is a function call that can help you debug or solve a problem you’re currently experiencing while developing a module.

Caution

puppetlabs/stdlib has four major releases, not all of which are compatible with end-of-life releases of Puppet. If you are using an unsupported version of Puppet, you might find situations in which the puppet module tool will not install modules due to version dependency conflicts.

When writing a public module that depends on stdlib functions, determine the minimum version of stdlib your module depends on. If all of the features you require are available in stdlib v4.12, declare that as the minimum version in your module to make life much easier for folks trying to use your module with obsolete versions of Puppet. (Caveat emptor.)

Always Use Parentheses

In previous versions of Puppet, and in the Ruby language upon which Puppet is based, it was popular to leave off parentheses when calling functions. In a top-level manifest, the code warning $message would generally evaluate the same as warning($message). Unfortunately this is not always true when used within enclosed blocks, such as lambdas, where there is an implicit context. This requires you to be constantly vigilant about usage, especially when code is copied from one context to another.

Always implement functions with parentheses around the parameter input.

Functions for Logging and Debugging

If you have access to the logs of the Puppet server or agent that creates the catalog, alert(), crit(), debug(), emerg(), err(), info(), and warning() are useful for adding debug logs to your code. These function calls are less impactful than the notify resource type, because they do not insert extra resources into the catalog and do not increment the change counter on the node report.

Due to their report of a convergence event, you should use notify resources when you do want to see a change reported in the convergence report (and the Puppet console).

If in doubt, use a function call rather than a notify resource.

fail() terminates catalog compilation and returns a message. It is commonly used by module authors to stop a Puppet run when critical dependencies are not available. fail() is also valuable when decommissioning obsolete code. You can insert a fail() statement to make it clear that a module or code path has been intentionally removed, and supply a message explaining what the user should do to rectify the problem.

String Manipulation Functions

The list of string manipulation functions in puppetlabs/stdlib is fairly extensive. For a complete list, read the stdlib documentation. Here are a few noteworthy function calls:

split()

Converts a string into an array, splitting at a specific character. This is very useful for handling delimited inputs, or for performing directory and filename manipulation.

join()

From the puppetlabs/stdlib, you can use this to convert an array back into a string.

strip(), lstrip(), and rstrip()

These are useful for trimming white space from input. With Puppet, most input is machine generated, and stripping the data is rarely necessary, except perhaps to remove problematic newline characters.

downcase()

This accepts a string as input and returns it converted to lowercase. upcase() and swapcase() are also available.

Path Manipulation

You can use dirname() and basename() to parse the filename or directory name out of a fully qualified file path. This is often very useful for file manipulation.

You can also use split(), join(), and array indexing to perform more complex path manipulation if necessary. This approach can be useful when working with older releases of puppetlabs/stdlib, which do not provide the dirname() function call.

Input Validation Functions

puppetlabs/stdlib and Puppet data types provide a rich assortment of input validation features. In many cases, the calls simply accept an input parameter and produce a useful failure message if the input fails validation. In other cases, you can use function calls with conditional logic to declare the fail() function.

Here’s a recommendation regarding input validation: unlike your typical web application, you probably don’t need to validate every data input as a possible attack vector, especially if the data is sourced from the node facts. Data supplied by the node is returned directly to the node and is unlikely to have a tangible impact on any other node in your infrastructure. The goal of input validation in a module should focus on ensuring the values are sane or usable for the methods intended. Sanitizing the data values also enforces a passive resistance to most forms of tampering.

If you are operating in a multitenant environment and cannot trust the authors of your code, you need to ensure security above the code layer. In this situation, we strongly recommend using a code review process to audit code and data before it is deployed. For a deeper dive into release management concerns in multiteam environments, see Chapter 9.

Do, however, consider validating inputs that might be executed locally to the Puppetmaster via function calls, or might be exported to other nodes in the infrastructure.

Input validation can create problems when it rejects otherwise valid inputs. Use validation to improve user experience by producing more useful information than the user would otherwise have by allowing malformed data to create failures elsewhere.

We discuss input validation in more depth in Chapter 4.

Data validation functions

These function calls accept an input and automatically cause the Puppet run to fail with a useful error if the input does not match the type expected, as demonstrated in the following code:

assert_type(Stdlib::Absolutepath, $provided_path) |$expected, $actual| {
  fail("The path provided was ${actual}, should be ${expected}")
}

Each value can be checked against the expected data type in this manner. Because the assert_type() core function can validate any data type, it provides an extensible replacement for a long list of validate_type() functions in stdlib: validate_absolute_path(), validate_bool(), validate_array(), and so forth have all been made obsolete by this general-purpose function.

Having a general-purpose test in Puppet’s core functionality ensures that any data type provided by any module can be tested. Here are some examples of replacements for the obsolete validate functions:

Old function Data-type test
validate_numeric($input) assert_type(Numeric, $input)
validate_float($input) assert_type(Float, $input)
validate_ip_address($input) assert_type(Stdlib::IP::Address, $input)
validate_v6_address($input) assert_type(Stdlib::IP::Address::v6, $input)
validate_mac_address($input) assert_type(Stdlib::MAC, $input)
validate_domain_name($input) assert_type(Stdlib::Fqdn, $input)

Data type comparison

In the same vein, the ip_type() functions provided by stdlib have been made obsolete by the built-in conditional. As shown in the code that follows, a value on the lefthand side of the =~ and !~ operators will be checked against the type on the righthand side. This can be any Puppet data type.

if ($provided_path =~ Stdlib::Absolutepath) {
  do something
}

Value existence comparison

There do remain some useful validation functions in stdlib focused on evaluating values within data types.

The has_interface_with(), has_ip_address(), and has_ip_network() functions use the interfaces facts to validate that the supplied IP address, network name, or interface is present on the node. Because these tests rely on node-supplied facts, it is possible to spoof them from a compromised node.

Most of the other data-checking functions have been made obsolete by methods specific to their data type. For example, grep(), has_key(), and member() have been replaced by the in operator, as shown here:

unless $ensure in ['present','absent'] {
  fail("$ensure must be 'present' or 'absent'")
}

if 'hacker' in $users {
  warning("The hacker has access here.")
}

Catalog Tests

The following functions return a Boolean value if a resource exists in the catalog:

  • defined()

  • defined_with_params()

These functions do a similar test, but declare the missing resource if it already exists:

  • ensure_packages()

  • ensure_resource()

At first glance, these functions appear to be a useful way to solve duplicate resource declaration issues, as illustrated here:

unless defined(Package['openjdk']) {
  package { 'openjdk':
    ensure => 'installed',
  }
}

Unfortunately, this code won’t work, because it’s parse order-dependent. If OpenJDK is added to the catalog after this block is evaluated, a resource conflict will still be created, as demonstrated here:

notice( defined(Package['openjdk']) ) # returns false

package { 'opendjk':
  ensure => 'installed',
}

notice( defined(package['openjdk']) ) # returns true

You might think the fix would be to wrap every potentially conflicting resource in a defined() test, or to declare every resource using the ensure_resource() function calls. Doing so creates other potential issues, such as the following:

ensure_resource('package', 'jre', {'ensure' => '1.7.0'})
ensure_resource('package', 'jre', {'ensure' => '1.8.0'})

This example raises a duplicate resource declaration error. The same code written using the defined() function call would result in nondeterministic and potentially nonidempotent Puppet runs. defined_with_params() works around the problem but creates other issues.

Managing conflicts using defined() or ensure_resource() also subtly violates the SoC principle; it requires that all modules use the same approach to handling package version tests. To update the test or resource declaration in one module, you would need to update all potentially conflicting modules.

These function calls have only two valid use cases:

  • A defined type can safely use these tests to ensure that it declares a resource one time, even when invoked more than once.

  • Classes within the same module could safely declare the same resource by using ensure_resource().

When dealing with the problem in a larger scope, there are two ways to effectively solve the problem:

  • Declare the common requirements as virtual resources, which can be safely realized by each dependent class.

  • Move the shared resources to their own class, which can be safely included by each dependent class.

You’ll need to determine which solution is the most robust and understandable for your needs.

Data Transformation

Data transformation is the act of manipulating data in one or more ways. We’ve already seen a few examples of data transformation, such as Example 3-16, in which we converted an array of integers into a list of filenames.

Puppet does not transform data in place; data can be transferred when passing it between classes or transformed when defining a new variable.

You should avoid transforming data unless necessary. When you must transform data, do so in the same class that initially defines the data or within the init manifest of the module that consumes the data.

Take, for example, the following list of filenames:

files:
  - foo.txt
  - bar.txt
  - baz.txt
directory: /tmp

In Example 3-21, we use that data in a role and profile to make use of a file implementor.

Example 3-21. Classes utilizing the YAML data
class roles::myfiles {
  Array  $files     = lookup('files')
  String $directory = lookup('directory')

  class { 'profiles::myfiles':
    files     => $files,
    directory => $directory,
  }
}

class profiles::myfiles (
  Array  $files,
  String $directory,
) {

  $apnfiles = prefix($files, $directory)

  class { 'create_files':
    files => $apnfiles,
  }
}

class create_files (
  Array $files,
) {
  file { $files:
    ensure => 'present',
  }
}

This somewhat confusing chunk of code retrieves data from Hiera and passes it to the profile. The data is transformed in profiles::myfiles, with the transformed data being sent to the create_files class. Imagine for a moment that the file resource contained in the class create_files was producing an unusual result. Our first reaction would be to look at the class create_files and then perhaps to look at the data in Hiera that feeds role::myfiles. Unfortunately, we would not be seeing the full picture, because the data is transformed in profiles::myfiles. In this case, debugging will go slowly because we must follow the path by which data transverses our codebase to identify the step where the transformation takes place.

This example could be improved significantly using one of several approaches:

  • Store the data in its final form.

  • Transform the data in Hiera using interpolation tokens.

  • Transform the data in the class  create_files.

In this case, the correct solution would be to store the absolute pathname in Hiera. When that is not feasible, the next best solution is usually to pass the data verbatim to the module, transforming the data in the same class that applies it. Even though this approach might violate the DRY principle, it’s much simpler to debug.

There are a number of cases for which data must be transformed inside the module. One such use case for which the default of one parameter should be based on the value supplied to another parameter. You can see examples of this in Chapter 4.

Whenever possible, it’s best to avoid passing data needlessly through multiple classes. The Hiera data terminus can be very helpful this way. For an in-depth discussion of the pros and cons of various approaches relating to Hiera, see Chapter 6.

Templates

Templates provide a traditional way of separating presentation logic from business logic. In the case of Puppet, templates allow us to interpolate data from Puppet into files, creating a layer of separation between the source configuration files and the logic used to populate those files.

ERB Templates

The Embedded Ruby (ERB) template parser provided by the template() and inline_template functions allow the use of Ruby language interpolation within a file. Puppet has supported this since its early days, and it is used in many other systems than Puppet.

There are a few best practices relating to the use of ERB templates:

Never source a template from another module. This violates SoC, and often results in problems down the line when someone changes the template in the other module without realizing your module will be affected. If you need to pass templates between modules, render the template in the source module, and pass the template in its rendered form as parameter data.

You should try to avoid referencing out-of-scope variables within a template. And you should absolutely avoid referencing variables from other modules. The reason for this recommendation is that it’s often difficult to determine which variables are being used by a template, especially when reviewing Puppet code, as illustrated here:

class alpha {
  file { '/etc/alpha.conf':
    content => template('alpha/alpha.conf.erb'),
  }
}

template alpha.conf.erb:
  # This will silently fail if beta module changes
  important setting = '<%= scope["beta::foo"] %>'

This situation hides the out-of-scope variable down in a data file (the template). Besides creating maintenance problems, out-of-scope variable lookup methods have inconsistent behaviors when tested as a Boolean value, and evaluate to true in unexpected situations.

Instead, you should copy out-of-scope variables into the manifest’s scope and reference the local variable, like so:

class alpha {
  # Get necessary value from related module 'beta'
  $important_setting = $beta::foo

  file { '/etc/alpha.conf':
    content => template('alpha/alpha.conf.erb'),
  }
}

template alpha.conf.erb:
  # this won't silently become nil if the variable reference is no longer good
  important setting = '<%= @important_setting %>'

Referencing only local variables guarantees that any variable used by your template is defined in the same class that declares the template. This also helps avoid subtle bugs created when out-of-scope variables change names and are treated as nil values by the template rather than throwing an exception.

EPP Templates

Embedded Puppet Programming Language (EPP) templates have been available in all versions of Puppet from 4.0 onward.

EPP is stylistically similar to ERB templating; however, it uses Puppet language variables, functions, and iterators rather than embedded Ruby. The advantage of using EPP templates over ERB templates is that EPP templates are written in the same language used to write your modules.

EPP provides enhanced security features, permitting you to pass an explicit list of parameters to the template, as shown in Example 3-22. The template cannot query variables that exist outside the template. This provides a huge maintainability improvement.

Example 3-22. Customizing an EPP template with parameters
$example_content = epp('example_module/template.epp', {
  'arg_a' => 'Value 1',
  'arg_b' => 'Value 2',
})

Always pass variables into your EPP templates using this explicit syntax. Within the template, declare the parameters in a comment block on the first line. Input type checking works exactly the same as in classes:

<%- | String[1] $arg_a,
      String[1] $arg_b
    |-%>

arg_a is <%= $arg_a %>
arg_b is <%= $arg_b %>

Don’t declare EPP input parameters with a resource declaration; instead, either assign the output of the EPP template to a variable and pass that to the resource, or assign the variables to a hash and pass that to the EPP statement (both approaches are demonstrated in Example 3-22).

The general recommendations for ERB templates also apply; do not reference out-of-scope variables, and never render templates stored outside the scope of your module.

EPP Versus ERB

The ability to explicitly pass input parameters to EPP templates (and have their data type checked) is a significant advantage over the ERB templating engine. You should seriously consider adopting EPP templates for this reason alone.

ERB is a very common templating language in Ruby frameworks. As a result, it enjoys a lot of tooling that isn’t available with EPP templates. If you are a Ruby shop, this might be an important issue for you.

Otherwise, the choice between the two comes down to whether you want to write your templates in the Puppet Ruby DSL.

Other Language Features

There are many language features that we haven’t covered in this quick run-through of Puppet features and conceptual design, but we do delve into them later in the book as follows:

  • Resource relationships, exported resources, metaparameters, and virtual resources (discussed in Chapter 5)

  • Classes and defined types (discussed in Chapter 4)

  • Node statements (discussed in Chapter 8)

  • Puppet node convergence run stages (discussed in Chapter 7)

Summary

This chapter discussed both good and bad coding practices, and provided an overview of useful function calls. Applying clean coding practices as discussed in this chapter will help make your code easier to understand and thus easier to maintain. In many cases, simple and clean code will often result in fewer defects, as well.

Takeaways from this chapter include the following:

  • Apply common development principles to improve the quality of your code.

  • Reduce the amount of code in your Puppet manifests.

  • Separate your code from your resource declarations.

  • Use clearly named variables to clarify the purpose of your code.

  • Separate your code into modules for reusability.

  • Avoid creating additional scope needlessly.

  • Use Puppet’s built-in and stdlib function calls to enhance your code.

  • Be extremely careful with scope when using templates.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.21.76.0