Learn Puppet - Facts, Variables & Facter
Episode 6 of 23

Learn Puppet - Facts, Variables & Facter

In this episode we dig into the data behind every node: the facts gathered by Facter, how to use them in manifests, variables and their scope, as well as if, case, unless, and string interpolation.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 5 you learned how to organize classes and modules and to separate roles from profiles. The module structure is now clean, but there's still one ingredient unused: data about the node itself. In episode 6 we fill that gap with facts, variables, and Facter.

Facter is the tool running on every agent that collects system information — operating system, memory, network, processors — and hands it to Puppet. With facts, the same manifest can produce different configurations depending on the machine it runs on.

Getting to Know Facter

Facter is a Ruby library shipped with the agent. During an agent run, Facter executes many plugins to collect data, then merges the results into a single structure called facts. Facts are sent to the server along with the catalog request, so the server can compile a catalog specific to each node.

Run Facter directly in the terminal to see what it contains:

Show all facts as JSON
facter --json
facter os
facter memory.system.total
facter networking.ip

facter --json dumps hundreds of keys. If you only need one fact, add the fact name at the end of the command, and Facter will display its value directly.

Note

Facts are persistent per node and are resent on every run. If a node hasn't run its agent for weeks, its facts can go stale — make sure monitoring alerts you when a node hasn't checked in for a long time.

Important Facts

Most facts are organized as nested hashes. The groups you'll use most often:

FactExample valueUse
os.nameUbuntuDistribution name
os.familyDebian or RedHatOS family for choosing the package manager
os.release.major22Major OS version
memory.system.total4.00 GiBTotal memory
processors.count4Number of CPU cores
networking.ip10.0.0.12Node's main IP
virtualvmware or physicalWhether the node is a VM
hostname and fqdnweb01.example.comNode identity

The os.family fact is the most decisive for manifest decisions: for the RedHat family the package is named httpd, while for Debian it's apache2.

Using Facts in Manifests

Inside manifests, facts are accessed through the $facts variable as a nested hash:

Using facts inside a class
class profile::motd {
  file { '/etc/motd':
    ensure  => file,
    content => "Node ${facts['hostname']} berbasis ${facts['os']['family']}\n",
  }
}
 
if $facts['memory']['system']['total_bytes'] < 2147483648 {
  notify { 'low-memory':
    message => 'Memori node kurang dari 2 GiB',
  }
}

Notice the $facts['os']['family'] pattern. Facter calls hashes inside hashes, so each level is accessed with square brackets. This approach replaces the old top-scope variables like $::osfamily that are no longer recommended.

Important

Don't use $::osfamily in new code. The legacy top-scope variables still work, but $facts has a much clearer source and doesn't depend on declaration position.

Variables and Scope

Variables in Puppet are immutable — once set, their value can't be changed in the same scope. They're declared with the $ sign:

Variable declaration
$app_port = 8080
$app_user = 'devnull'
 
file { '/etc/app/config.yml':
  ensure  => file,
  content => "port: ${app_port}\nuser: ${app_user}\n",
}

Scope determines where a variable is visible:

  • Top scope — declared outside classes and nodes; accessed with the $:: prefix.
  • Class scope — class variables are only visible inside that class.
  • Class parameters — formal inputs that make a class easy to customize.

Besides $facts, Puppet provides other special variables: $trusted contains data from the node's certificate, and $server_facts contains server identity. Both can be trusted because nodes can't forge them.

Conditionals: if, unless, and case

Puppet supports three main conditional forms. if is used for general branching:

if and elsif
if $facts['os']['family'] == 'RedHat' {
  $web_pkg = 'httpd'
} elsif $facts['os']['family'] == 'Debian' {
  $web_pkg = 'apache2'
} else {
  $web_pkg = undef
}

unless runs its block only when the condition is false — the opposite of if:

unless for exceptions
unless $facts['os']['family'] == 'Windows' {
  package { 'openssh-server':
    ensure => installed,
  }
}

case is good for many possible values of a single variable:

case for selecting values
case $facts['os']['family'] {
  'RedHat': { $pkg_mgr = 'dnf' }
  'Debian': { $pkg_mgr = 'apt' }
  'Suse':   { $pkg_mgr = 'zypper' }
  default:  { fail("Tidak mendukung keluarga ${facts['os']['family']}") }
}

There's also the selector, an expression form that returns a value:

One-line selector
$ssl_mode = $facts['os']['family'] ? {
  'RedHat' => 'enable',
  default  => 'disable',
}

Tip

Compare case and the selector: case runs a block of code, while a selector always produces a value. If you only need to fill a variable, the selector is more concise.

String Interpolation

Double-quoted strings support variable interpolation. The syntax is $var for simple variables, and $facts['key'] to access hash members inside a string:

Interpolation with facts
$banner = "Selamat datang di ${facts['os']['name']} ${facts['os']['release']['major']}"
 
file { '/etc/banner':
  ensure  => file,
  content => $banner,
}

Curly braces are used when interpolating a variable containing square brackets, so the parser doesn't get confused. Always make sure variables inside a string are defined before use, because Puppet will fail at compilation if they aren't.

Custom Facts

Sometimes the built-in facts aren't enough — for example, you need to read a file yourself or combine several sources. There are two ways to add custom facts:

External facts — the simplest: just drop a text, YAML, or JSON file in the facts.d directory:

External fact from a text file
echo "role=web-server" | sudo tee /etc/puppetlabs/facter/facts.d/role.txt
facter role
facts.d/region.yaml
region: ap-southeast
datacenter: dc-01

Ruby custom facts — for more complex logic, write them in lib/facter/ inside a module:

lib/facter/custom_role.rb
Facter.add('custom_role') do
  setcode do
    if File.exist?('/etc/role')
      File.read('/etc/role').strip
    else
      'default'
    end
  end
end

Facter runs that Ruby file during its bootstrap, and the result becomes available like a built-in fact: $facts['custom_role'].

Warning

External facts must be simple text without strange characters, because they're parsed with key=value rules. For long values or values containing spaces, the YAML format is safer.

Conclusion

In this episode you opened the door to truly adaptive manifests: node data now flows into configuration decisions.

  • Facts are node data — collected by Facter on every run and sent along with the catalog.
  • The os, memory, networking, and processors groups are the most commonly used facts for branching.
  • $facts accesses data in manifests — choose the OS family, package name, and node-specific values.
  • Variables are immutableif, unless, case, and selectors control the decision flow.
  • Custom facts extend node data for your organization's unique needs.

Now data flows into manifests. The next step is separating data from code entirely: in episode 7 you'll learn Hiera — Hierarchical Data, how to store layered configuration in YAML and JSON, get to know the lookup function, and secure secrets with hiera-eyaml. See you there!

Learn Puppet - Facts, Variables & Facter | Learn Puppet