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.

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.
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:
facter --json
facter os
facter memory.system.total
facter networking.ipfacter --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.
Most facts are organized as nested hashes. The groups you'll use most often:
| Fact | Example value | Use |
|---|---|---|
os.name | Ubuntu | Distribution name |
os.family | Debian or RedHat | OS family for choosing the package manager |
os.release.major | 22 | Major OS version |
memory.system.total | 4.00 GiB | Total memory |
processors.count | 4 | Number of CPU cores |
networking.ip | 10.0.0.12 | Node's main IP |
virtual | vmware or physical | Whether the node is a VM |
hostname and fqdn | web01.example.com | Node 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.
Inside manifests, facts are accessed through the $facts variable as a nested hash:
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 in Puppet are immutable — once set, their value can't be changed in the same scope. They're declared with the $ sign:
$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:
$:: prefix.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.
Puppet supports three main conditional forms. if is used for general branching:
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 $facts['os']['family'] == 'Windows' {
package { 'openssh-server':
ensure => installed,
}
}case is good for many possible values of a single variable:
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:
$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.
Double-quoted strings support variable interpolation. The syntax is $var for simple variables, and $facts['key'] to access hash members inside a string:
$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.
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:
echo "role=web-server" | sudo tee /etc/puppetlabs/facter/facts.d/role.txt
facter roleregion: ap-southeast
datacenter: dc-01Ruby custom facts — for more complex logic, write them in lib/facter/ inside a module:
Facter.add('custom_role') do
setcode do
if File.exist?('/etc/role')
File.read('/etc/role').strip
else
'default'
end
end
endFacter 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.
In this episode you opened the door to truly adaptive manifests: node data now flows into configuration decisions.
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.if, unless, case, and selectors control the decision flow.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!