Learn Puppet - Custom Resources, Functions & Providers
Series/Learn Puppet/Episode 16
Episode 16 of 23

Learn Puppet - Custom Resources, Functions & Providers

Extend Puppet beyond built-in resources: define defined types, create custom facts for Facter, and write functions with the modern functions API. Also understand how per-platform providers like apt, yum, and systemd work, and how to create a custom provider.

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

Introduction

In episode 15 you learned manifest best practices, including idempotency and the roles and profiles pattern. All of that runs on Puppet's built-in resources like package, service, and file. But sometimes the need is more specific: modeling a recurring business entity, adding facts specific to our system, or computing values at catalog compilation time.

Puppet provides three extension layers to answer those needs: defined types to wrap several resources into one reusable unit, custom facts to add data Facter collects, and functions for data transformation logic on the catalog side. Behind every resource are providers that determine how the resource is applied on different platforms.

In this episode we'll cover creating defined types, custom facts, functions with the modern functions API, provider comparisons across platforms like apt, yum, and systemd, and the principles of creating a custom provider.

Defined Types: Wrapping Resources

A defined type is a collection of resources wrapped in one named unit. Unlike a class, a defined type can be used multiple times with different names in a single catalog.

manifests/define/user_account.pp
define user_account (
  String $ensure = 'present',
  Array[String] $groups = [],
) {
  user { $name:
    ensure     => $ensure,
    groups     => $groups,
    membership => 'minimum',
  }
 
  file { "/home/${name}":
    ensure => directory,
    owner  => $name,
    group  => $name,
  }
}

The defined type name is written without colons because it's defined in the manifests/define/user_account.pp file. To use it:

Using a defined type
user_account { 'deploy':
  groups => ['www-data', 'docker'],
}
 
user_account { 'ci-runner':
  ensure => absent,
}

Tip

The resource name (deploy, ci-runner) is available inside the definition as the $name variable and is automatically used for the internal resource titles. Make sure the names are unique to avoid resource duplication across the catalog.

Custom Facts for Facter

Facter provides built-in facts like os, networking, and memory. When you need custom data, you add a custom fact as a Ruby file in lib/facter/ inside a module.

lib/facter/uptime_hours.rb
Facter.add('uptime_hours') do
  setcode do
    seconds = Facter.value('uptime_seconds').to_i
    (seconds / 3600.0).round(2)
  end
end

To validate the new fact on a local machine, run facter uptime_hours and see the result. PDK also provides a scaffold for facts:

Create a custom fact skeleton with PDK
pdk new fact uptime_hours

The custom fact can then be used in manifests like a built-in fact, for example $facts['uptime_hours'], and can serve as the basis for classification rules in the node classifier we covered in episode 13.

Functions with the Modern Functions API

Functions transform data at catalog compilation time. Modern Puppet recommends the functions API with Puppet::Functions.create_function, not the old Ruby-module-based API.

lib/puppet/functions/join_words.rb
Puppet::Functions.create_function(:join_words) do
  dispatch :join_words do
    param 'Array[String]', :words
  end
 
  def join_words(words)
    words.join(' ')
  end
end

A newly defined function can be called directly from manifests:

Calling a custom function
$words = ['selamat', 'datang', 'di', 'puppet']
$sentence = join_words($words)
notify { $sentence: }

Besides writing your own functions, Puppet provides many built-in functions from stdlib like join, split, flatten, and map. Use the built-in functions first before writing a custom function.

Providers: The Bridge to Platforms

A provider is the engine behind every Puppet resource. The package resource uses the apt provider on Debian/Ubuntu and the yum or dnf provider on RedHat. The service resource uses the systemd, init, or service provider depending on the init system.

ResourceProviderPlatform
packageaptDebian, Ubuntu
packageyum / dnfRHEL, CentOS, Fedora
packagegemRubyGems (all platforms)
servicesystemdModern distributions
serviceinit / serviceSysV init
useruseraddCommon Linux

Puppet chooses the provider automatically based on platform facts and configuration. This choice can be influenced with the provider parameter on a resource, for example provider => 'systemd'.

Important

Don't hardcode a provider unless you really have to. Letting Puppet choose the provider keeps manifests portable across OSes. Forcing the apt provider on a RHEL system will just make the run fail.

Creating a Custom Provider

Providers are written as Ruby files in lib/puppet/provider/<type>/<provider_name>.rb, with lib/puppet/type/<type>.rb for the resource type. Providers commonly use the confine directive to restrict the platforms they support.

lib/puppet/provider/package/custom_apt.rb
require 'puppet/provider/package'
 
Puppet::Type.type(:package).provide(:custom_apt,
  parent: Puppet::Provider::Package::Apt,
  source: :apt) do
  confine operatingsystem: :debian
 
  commands aptget: 'apt-get'
 
  def install
    aptget('-y', 'install', @resource[:name])
  end
 
  def query
    # kembalikan hash status paket, atau nil jika tidak terinstall
  end
end

Warning

Creating a custom provider is advanced work and should be avoided when a built-in resource or a Forge module already suffices. A wrong provider can damage the system. When possible, inherit from an existing provider (like the parent: example above) so its idempotent behavior is inherited.

The file location determines the scope of use. A provider placed in a module's lib/ is only available when that module is used, while a provider in an environment's modules/ directory applies to that whole environment.

Conclusion

In this episode 16 you understood Puppet's three extension layers: defined types to wrap repeatedly used resources, custom facts to add Facter data, and functions with the modern functions API for data transformation at catalog compilation. You also understood the provider's role as the bridge from resources to platforms, from apt and yum to systemd, plus the principles of creating custom providers.

Key takeaways:

  • Defined types wrap several resources into a reusable unit with $name as the identity.
  • Custom facts are written in lib/facter/ as Ruby files and tested with facter.
  • The modern functions API uses Puppet::Functions.create_function with a dispatch declaration.
  • Providers make resources work across platforms; let Puppet choose them automatically.
  • Custom providers are only for extreme needs and should inherit from existing providers.

When your node count reaches the hundreds, the data Puppet collects can be very valuable. In episode 17 we'll cover Learn Puppet - Advanced PuppetDB Queries & Exported Resources: querying the API for facts, reports, and resources, consuming data from external tools, and exported resource patterns for inventory, monitoring, and cross-node network configuration.