Variable scoping
Puppet
In Puppet, the combined configuration to be applied to a host is called a catalog, and the process of applying it is called a run.
The Puppet client software is called the agent. Puppet calls the definition of the
host itself a node. The Puppet server is called the master.
So what's a scope? Each class or node introduces a new scope.
Four scopes are available: top scope, node scope, parent scope, and local scope.
- Top scope is anything declared in site.pp or imported manifests. Top scope can be explicitly accessed by prepending :: to a variable. It is best practice to write fact variables as $::var so as to use the fact at top scope, thus preventing the variable from being overwritten anywhere.
- Node scope is the scope created by the enclosing brackets of a node definition. Node scope is unfortunately anonymous, so there is no way to explicitly retrieve it. A variable set at node scope will still be available in local scope unless it is overridden at local scope or parent scope.
- Local scope is the scope of a single class or defined type.
- Parent scope is the scope of a class that is explicitly inherited through use of the inherits keyword.
site.pp file tells Puppet where and what configuration to load for our clients. We're going to store this file in a directory called manifests under the /etc/puppet directory. Manifest is Puppet's term for files containing configuration information. Manifest files have a suffix of .pp.
Create a site.pp file:
# /etc/puppet/manifests/site.pp node "puppet-agent" { file { "/tmp/hello" : content => "hello bogotobogo.com" } }
On 'agent':
root@puppet-agent:/# puppet agent --test Info: Caching certificate_revocation_list for ca Info: Retrieving plugin Info: Caching catalog for puppet-agent.ec2.internal Info: Applying configuration version '1419574947' Notice: /Stage[main]/Main/Node[puppet-agent]/File[/tmp/hello]/ensure: defined content as '{md5}774f1e8eb478358d3cf5713c2900397c' Info: Creating state file /var/lib/puppet/state/state.yaml Notice: Finished catalog run in 0.02 seconds
If we open /tmp/hello file, it has:
hello bogotobogo.com
Let's look at Puppet's components, configuration language, and capabilities. Puppet manifests are made up of a number of major components:
- Resources: Individual configuration items
- Files: Physical files you can serve out to your agents
- Templates: Template files that you can use to populate files
- Nodes: Specifies the configuration of each agent
- Classes: Collections of resources
- Definitions: Composite collections of resources
Let's add our node definition to /etc/puppet/manifests/site.pp. In Puppet manifests on 'master', agents are defined using node statements as shown below:
node 'puppet-agent' { package { 'vim' : ensure => present, } }
We specified the node name for a node definition. The name is enclosed in quotes, and then specified the configuration that applies to it inside curly braces { }. The client name can be the hostname or the fully qualified domain name of the client.
We specify a resource stanza in our node definition. It will make sure that the vim package is installed on the host 'puppet-agent'. We can run Puppet on 'puppet agent' and see what action it has performed:
ubuntu@puppet-agent:~$ sudo puppet agent --test Info: Retrieving plugin Info: Caching catalog for puppet-agent.ec2.internal Info: Applying configuration version '1419623309' Notice: Finished catalog run in 0.03 seconds ubuntu@puppet-agent:~$
Puppet has installed the vim package. However, it is not generally best practice to define resources at node level. Resources belong in classes and modules. Let's strip out our vim resource and include the sudo class instead:
node 'puppetagent.example.com' { include sudo }
Here we specify an include directive in our node definition. We specifies a collection of configurations, called a class which we want to apply to our host. There are two ways to include a class:
- This first syntax is simple:
node /puppetagent/ { include ::sudo }
- This second syntax allows parameters to be passed into the class, and this is called parameterized classes. This allows classes to be written generally and then utilized specifically, which increases the reusability of Puppet code. Notice that the syntax for including a class is very similar to the syntax for a normal Puppet resource. Modules are self-contained collections of Puppet code, manifests, Puppet classes, files, templates, facts, and tests, all for a specific configuration task. Modules are usually highly reusable and shareable.
The double-colon syntax explicitly instructs Puppet to use top scope to look up the sudo module.
node /puppetagent2/ { class { '::sudo': users => ['k', 'contact'], } }
The next step in our node configuration is to create a sudo module.
A module is a collection of manifests, resources, files, templates, classes, and definitions. A single module would contain everything required to configure a particular application. For example, it could contain all the resources specified in manifest files, files, and associated configuration to configure Apache or the sudo command on a host.
We will create a sudo module and a sudo class. Each module needs a specific directory structure and a file called /etc/puppet/modules/sudo/manifests/init.pp. This structure allows Puppet to automatically load modules. To perform this automatic loading, Puppet checks a series of directories called the module path. This path is configured with the modulepath configuration option in the [master] section of the /etc/puppet/puppet.conf file. By default, Puppet looks for modules in the /etc/puppet/modules and /usr/share/puppet/ modules directories, but we can add additional locations if required:
[master] modulepath = /etc/puppet/modules:/var/lib/puppet/modules:/opt/modules
We need create a module directory and file structure under the directory /etc/puppet/modules :
root@puppet:~# mkdir -p /etc/puppet/modules/sudo/{files,manifests} root@puppet:~# touch /etc/puppet/modules/sudo/manifests/init.pp
The sudo module's init.pp file:
class sudo { package { 'sudo': ensure => present, } if $::osfamily == 'Debian' { package { 'sudo-ldap': ensure => present, require => Package['sudo'], } } file { '/etc/sudoers': owner => 'root', group => 'root', mode => '0440', source => "puppet://$::server/modules/sudo/etc/sudoers", require => Package['sudo'], } }
Our sudo module's init.pp file contains a single class, also called sudo. There are three resources in the class, two packages and a file resource. The first package resource ensures that the sudo package is installed, ensure => present. The second package resource uses Puppet's if/else syntax to set a condition on the installation of the sudo-ldap package.
Puppet will check the value of the operatingsystem fact for each connecting client. If the value of the $::osfamily fact is Debian, then Puppet should install the sudo-ldap package. Operating system family is just a name Puppet uses for binary-compatible groups of distributions; for example, Debian, Ubuntu, and Mint all share the osfamily Debian.
Last, in this resource we've also specified a new attribute, require. The require attribute is a metaparameter. Metaparameters are resource attributes that are part of Puppet's framework rather than belonging to a specific type. They perform actions on resources and can be specified for any type of resource.
The file resource, File["/etc/sudoers"], which manages the /etc/sudoers file. Its first three attributes allow us to specify the owner, group, and permissions of the file. In this case, the file is owned by the root user and group and has its mode set to 0440.
The next attribute, source, allows Puppet to retrieve a file from the Puppet source and deliver it to the client. The value of this attribute is the name of the Puppet source and the location and name of the file to retrieve:
puppet://$::server/modules/sudo/etc/sudoers
Let's break down this value. The puppet:// part specifies that Puppet will use the Puppet file server protocol to retrieve the file.
The $::server variable contains the hostname of our Puppet server. One handy shortcut is to just remove the server name. Then Puppet will use whatever server the client is currently connected to; for example, our source line would look like
puppet:///modules/sudo/etc/sudoers
All files in modules are stored under the files directory which is considered the root of the module's file share.
In our case, we would create the directory etc under the files directory and create sudoers in this directory:
root@ip-172-31-45-62:~# mkdir -p /etc/puppet/modules/sudo/files/etc root@ip-172-31-45-62:~# cp /etc/sudoers /etc/puppet/modules/sudo/files/etc/sudoers
Now that we've created our Puppet module, we want to connect an agent that includes this module:
- It will install the sudo package.
- If it's an Ubuntu host, then it will also install the sudo-ldap package.
- Finally, it will download the sudoers file and install it into /etc/sudoers.
Now let's see this in action and include our new module on the agent we've created, "puppet-agent'. We'll use a node statement in /etc/puppet/manifests:
node 'puppet-agent' { include sudo }
When the agent connects, it will now include the sudo module. To connect we run the Puppet agent again and connected to the master:
ubuntu@puppet-agent:~$ sudo puppet agent --test
Puppet
Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization