Introduction:
This document provides a comprehensive overview of Puppet language and guides users through the process of writing modules. Puppet, a powerful configuration management tool, allows for efficient automation of system administration tasks. Understanding Puppet language and creating custom modules is crucial for maximizing its capabilities.
Puppet Language Basics
Let's discuss the building blocks of puppet code
1) Declarative nature:
Puppet is a declarative language, meaning you describe the desired state of your system rather than specifying the steps to achieve that state.
Here's an example,
package { 'nginx':
ensure => 'installed',
}
2) Manifests file:
Manifests are the fundamental units of Puppet code. They contain the instructions for Puppet to follow. Every manifest file has a resource declaration.
If the file is a module manifest then the resources are declared inside a Class statement and if the manifest if an environment manifest then the resources are declared inside a node statement. Puppet manifests are named with the .pp file extension.
Here's an example of a manifest file. This can be located in environment manifest folder.
class webserver {
file { '/etc/nginx/nginx.conf':
ensure => 'file',
source => 'puppet:///modules/nginx/nginx.conf',
notify => Service['nginx'],
}
service { ‘nginx’:
ensure => ‘running’,
enable => true,
}
}
Or,
node webserver01.example.com {
class { 'webserver::nginx': }
3) Classes:
Classes enables grouping of related code (resource declaration) and provide a way to organize and reuse Puppet code. A class encapsulates one or multiple resource declarations and is always given a name.
Simply applying a class to a node, in turn applies all resources within that class. A class is defined inside a manifest file within the modules folder. Classes are invoked by its name.
Here's an example of a class declaration:
class nginx {
package { 'nginx':
ensure => 'installed',
}
service { 'nginx':
ensure => 'running',
}
}
In the above example, note the syntax. A class is defined by class keyword, followed by the class name. In this example, 'nginx' is the name of the class. this is followed by an opening parenthesis. Inside this we have list of resources declared (package and service in this example) with respective attributes defined with $myparam = "value". Each parameter is comma separated. After we define required number of resources, the class statement is closed with a closing parenthesis.
4) Resources:
Resources are the basic building blocks in Puppet. They represent the various elements of a system that Puppet manages. Each resource represents a configurable component in a device (virtual or physical). For example, a user, file, application, service, etc. There are near to 25 different resources used in Puppet code.
Each resource represents a configurable parameter in a device, for example, in a server, 'user', 'service', 'application packages', 'files' and many more are such parameters that, if values of it are changed, will alters the behavior of the device.
To get more details of types of resources refer to the 3rd document in this series.
Here's an example of resource declaration. In this case we are defining the user resource. 'ensure', 'home', 'managehome', are the attributes of it.
user { 'ganesh':
ensure => 'present',
home => '/home/ganesh',
managehome => true,
}
5) Variables:
Puppet supports different types of variables to make code more flexible and reusable.
Here are some examples of variables declarations,
$ webserver = 'nginx'
package { $webserver:
ensure => 'installed',
}
$ address_array = [$address1, $address2, $address3]
$rule = "Allow * from $ipaddress"
In this example, the variable key '$ webserver' is declared inside the manifest file. Declaring a variable is done using '$' sign and value assignment is done using '=' sign. To call a variable inside the resource statement, we use again the '$' sign, followed by the variable key word.
Multiple variables can also be assigned from an array or hash.
Variables declared within a manifest has a global scope within the manifest, but in the same manifest inside a class statement one can assign a different value to the same variable again.
Puppet can resolve variables that are included in double-quoted strings; this is called interpolation.
$message = ‘hello world!’ #string
$ntp_service = ‘ntp’ #string
$size = 100 #number
$answer=true #Boolean
All above examples are scaler values where there is only a single value.
Using 'heredoc' in Puppet
'heredoc' is a way to include multiline strings in your Puppet code. It's particularly useful when you want to define a block of text, such as a configuration file or a script, without having to concatenate strings line by line.
here's a basic example,
$my_variable = @(END)
This is a multiline
string using heredoc
with variables like $variable_name
END
$my_variable is assigned a multiline string.
@(END) is the heredoc syntax, indicating the start of the multiline string.
The text between @(END) and the next occurrence of END is the content of the multiline string.
You can include variables within the heredoc, and Puppet will interpolate them.
Similarly,
$server_name = "webserver"
$configuration = @(CONFIG)
server {
listen 80;
server_name $server_name;
root /var/www/$server_name;
}
CONFIG
In this case, the value of $server_name will be interpolated inside the heredoc, so the resulting content of $configuration will include the actual value of the variable.
Global and local scope of variables,
$global_var = 'I am global'
class example {
{
$local_var = 'I am local'
notify { 'Local Variable': message => $local_var }
}
}
In this example, the global scope variables are accessible everywhere on the manifest, whereas the local variable defined inside the class is used within the class only.
A variable defined inside a node definition in environment manifest has a scope limited to that node only, example as stated below.
node 'webserver.example.com' {
$node_specific_var = 'local_to_the_node'
}
6) Iterations:
Puppet supports iteration through arrays and hashes, allowing you to manage multiple resources efficiently.
$packages = ['nginx', 'mysql', 'php']
package { $packages:
ensure => 'installed',
}
- Syntax and structure, including manifests, classes, and resources.
- Variables, conditionals, and iteration in Puppet.
Iterations using 'each' statement.
$binaries = ['facter', 'hiera', 'mco', 'puppet', 'puppetserver']
# function call with lambda:
$binaries.each |String $binary| {
file {"/usr/bin/${binary}":
ensure => link,
target => "/opt/puppetlabs/bin/${binary}",
}
}
An Arrays can contain multiple values.
$admingroups = [ ‘admin’ , ’user’ ] #array
$values = [‘admin’, ‘user’,123,true] … # array can contain multiple values of multiple data types as well.
$user = { ‘username’ => ‘ganesh’,
‘uid’ => ‘2010’, } #hashed array.
notify { “The ${ntp_service} is up and running” : } …. Using variable inside a string, the string will have to quote.
To use an element from an array, we use it as mentioned below.
notify { “the first element is ${admingroup[0]}”: } … here notify is a resource type.
To start testing this, create a manifest.pp file and add below lines of code.
$ntp_service = ‘ntpd’
notify { $ntp_service : }
notify { “the ${ntp_service} is running” : }
notify { $user [‘username’] : }
notify { “the user group is ${user[‘username’]}” : }
notify { “the user group is ${values[1]}” : }
Above are the various ways to use variables, variable arrays, etc.
7) Conditional statements:
Puppet allows, conditional logic to execute code based on certain conditions. Puppet conditional statements are most helpful when combined with facts or with data retrieved from external source. Puppet supports if and unless statements, case statements and selectors.
if - elsif - else
if $operatingsystem == 'Ubuntu' {
package { 'apache2':
ensure => 'installed',
}
} elsif $operatingsystem == 'CentOS' {
package { 'httpd':
ensure => 'installed',
}
} else $operatingsystem == 'Windows' {
package { 'apache':
ensure => 'installed',
}
}
case statement
case $facts['os']['name'] {
'RedHat', 'CentOS': {
include role::redhat
}
/^(Debian|Ubuntu)$/: {
include role::debian
}
default: {
include role::generic
}
}
"Unless" statements work like reversed if statements.
An ‘unless’ option can also be used if a certain condition is not fulfilled’, like as shown in below example. Let's understand it in comparison with 'if' statement.
if $facts[‘os’][‘family’] != ‘RedHat ’ {
notify { ‘RedHat’ : }
}
Or,
unless $facts[‘os’][‘family’] == ‘Debian’ {
notify { ‘RedHat’ : }
}
Selector statement
A selector can be used to populate a variable based on a certain condition. This is great where we want to set only one variable value but on multiple conditions.
$ntp_service = $facts[‘os’][‘family’] ? {
'RedHat => ntpd',
‘Debian => ntp’,
}
Using Facts and the Facter tool
By running the ‘$facter’ command puppet can collect information about your machine.
For accessing the values collected using Facter we use below syntax.
$ facter kernel
$ facter os.family
Inside the code we can use it with below syntax.
$facts[‘os’][‘family’]
Let’s practice the facts usage using a heredoc variable.
$display = @ (“END”)
Family: ${facts[‘os’][‘family’]}
OS: ${facts[‘os’][‘name’]}
Version: ${facts[‘os’][‘release’][‘full’]}
END
Managing Puppet resource relationships
If the ordering is not forced then it is the Puppet agent to handle the resource ordering. This is usually done in puppet 4 resource ordering is performed the order the resources are written or read from the manifest.pp file.
In case there are two resources mentioned in manifest that are not related to each other as a dependency then if one fails all rest of the resources would fail. Thus, the ordering of resource is important.
Making use of the Meta-Parameter such as Before or Require help to order the resource ordering.
We can set random resource ordering using the command,
$ sudo puppet apply --ordering=random <manifest.pp> …. With this we can apply / set dependency of the resources inside the manifest.
Using our manifest file code that we have created earlier in the session,
$ntp_conf = ‘#Managed by puppet
server 192.168.33.101 iburst
driftfile /var/lib/ntp/drift’
package { ‘ntp’ :
before => File [‘/etc/ntp.conf’],
}
file { ‘/etc/ntp.conf’:
ensure => ‘file’,
content => $ntp_conf,
owner => ‘root’,
group => ‘wheel’,
mode => ‘0644’,
require => Package [‘ntp’],
before => Service [‘NTP_Service’],
}
service { ‘NTP_Service’ :
ensure => ‘running’,
enable => true,
name => ‘ntpd’,
require => File [‘/etc/ntp.conf’],
}
We can also make use of Notify and Subscribe meta-parameters to automatically trigger a resource when a certain change happens in a other resource.
package { ‘ntp’:
before => File [‘/etc/ntp.conf’],
}
file {
owner => ‘root’,
group => $admingroup,
mode => ‘0664’,
ensure => ‘file’,
}
file { ‘/etc/ntp.conf’:
content => $ntp_conf,
notify => Service [‘NTP_Service’],
}
service { ‘NTP_Service’:
ensure => ‘running’,
enable => true,
name => ‘ntpd’,
subscribe => File [‘/etc/ntp.conf’],
}
In the above example, the Notify and Subscribe parameters are going to help check if there are changes in the ntp.conf file. And if yes, the Service [‘NTP_Service’] would be restarted.
Working with Modules and Classes
Puppet modules:
Puppet module is a collection of related code, data, and configuration files that work together to perform a specific task. Modules provide a way to organize and reuse Puppet code, making it easier to manage and scale infrastructure configurations. Puppet modules encapsulate resources, classes, files, and other elements necessary for a particular functionality or purpose.
Let's break down the key components and concepts related to Puppet modules.
Module structure:
A Puppet module typically follows a specific directory structure. Here is a basic example.
Follow the folder path, /etc/puppetlabs/code/environment/production/modules/webserver
Here the modules folder is located inside the environments folder and the module / class is usable inside the production environment only. In this case the nodes that are part of Production environment can read the code from this modules folder located inside environment/production.
Similar to the environment modules folder which is local to the environment, we also have a global modules that is placed above the environment folder and thus can be accessed by all nodes, irrespective of which environment it belongs to.
The path for global modules directory is, /etc/puppetlabs/code/modules.
The above directory structure allows bifurcation of code for different environments.
manifests: Contains Puppet manifests (code written in Puppet language).
files: For static files that the module uses.
templates: For template files used with the template function in Puppet.
lib: Optional, for custom Ruby libraries.
facts.d: Optional, for custom facts.
tests: For testing-related files.
metadata.json: Describes metadata about the module.
Manifests:
The primary Puppet code is written in manifest files (.pp files). The main manifest, often named init.pp, contains the primary class definition and other resource declarations.
A sample init.pp file in the webserver module would look similar as defined here,
class nginx {
package { 'nginx':
ensure => 'installed',
}
file { '/etc/nginx/nginx.conf':
ensure => 'file',
content => template('nginx/nginx.conf.erb'),
notify => Service['nginx'],
}
service { 'nginx':
ensure => 'running',
enable => true,
}
}
Using a Puppet module
First of all, to install a module from Puppet Forge, use below commands,
$ sudo puppet module install author_name/module_name
Modules that are already present / installed can be listed using the list command,
$ sudo puppet module list
Once a module is installed, we can include the module in your Puppet manifests using the include statement.
include webserver
we can also use the class syntax to include a specific class from the module.
class { 'mymodule::myclass':
# Class parameters go here
}
Creating a Puppet module
1) Module initialization:
To start creating module and / or write puppet code, we have to get the Puppet Development Kit installed on a standalone machine. This machine will be treated as the puppet development machine or environment. Once the PDK is installed here are some of the commands to get started.
To create a new module, use the pdk new module command.
$ pdk new module module_name
This command initializes a new module with a default directory structure. The internal files and folder are as show in the image above.
2) Code development:
Write Puppet manifest and other necessary files in their respective directories within the module. The manifest folder inside the module folder is where the init.pp (default) manifest resides. In this file we will have a class defined by the name of the module.
3) Testing of module:
Use tools like RSpec for unit testing and beaker for acceptance testing to ensure your module behaves as expected. These testing tools are just few from the list of similar tools available.
4) Metadata:
The metadata.json file inside the module directory structure is where we will have to update the information about the module. Information like, license type, owner's name, version info., etc.
5) Publishing / Sharing module:
Once the module is tested, if we want to publish it on public or enterprise platform for other developers / teams to use, we can push it to the Puppet Forge module repository.
$ puppet module push authorname-modulename
6) Classes:
Classes are named blocks of Puppet code that are stored in modules for later use and are not applied until they are invoked by name. They can be added to a node's catalogue by declaring them in your manifests.
Parameterized classes:
class nginx(
String $listen_address = '127.0.0.1',
Integer $listen_port = 80,
String $server_name = 'localhost',
String $document_root = '/var/www/html'
)
{
package { 'nginx':
ensure => 'installed',
}
------
Here, the string $listen_address, $listen_port, $server_name etc., are the parameters passed to the class nginx.
Templates used in Puppet:
Puppet, templates are a mechanism used to dynamically generate configuration files based on a set of parameters or variables. Templates enable you to create reusable and flexible configurations by separating the logic from the actual content of the configuration files. Puppet employs the Embedded Ruby (ERB) templating language for this purpose.
Template creation:
File extensions: Templates typically have a .erb file extension, indicating they use Embedded Ruby for dynamic content.
Location: Place your template file in the appropriate module's templates directory. Puppet conventionally organizes templates within the templates subdirectory of a module.
Embedded Ruby (ERB): Utilize ERB syntax to embed Ruby code within the template. This code will be evaluated dynamically when Puppet generates the configuration.
Comments