Deferred functions overview

Defer a function to retrieve a value on the agent.

The Deferred type instructs agents to execute a function locally to retrieve a data value at the time of catalog application. When compiling catalogs, functions are normally executed on the primary server, with results entered into the catalog directly. The complete and fully resolved catalog is then sent to the agent for application. You can choose to defer the function call until the agent applies the catalog, meaning the agent calls the function on the agent instead of on the primary server. This way, agents can use a function to fetch data directly, rather than having the primary server act as an intermediary.

The two most common reasons for deferring functions are:
  • To retrieve a value on the agent that the primary server doesn’t have access to, including sensitive information like passwords from a secrets store.
  • To use as a placeholder to describe intent and make your desired state as descriptive as possible.

Deferred function example

The following example shows a file with a template on the agent that defers two functions — the Vault password lookup and the epp template compilation.

The password lookup is deferred to run on the agent so that the primary server does not need to know the secret, and the epp() function is deferred to render the template after the password value is available.
$variables = {
  'password' => Deferred('vault_lookup::lookup',
                  ["secret/test", 'https://vault.docker:8200']),
}

# compile the template source into the catalog
file { '/etc/secrets.conf':
  ensure  => file,
  content => Deferred('inline_epp',
               ['PASSWORD=<%= $password.unwrap %>', $variables]),
}
The Deferred object initialization signature returns an object that you can assign to a variable, pass to a function, or use like any other Puppet object. For example:
Deferred( <name of function to invoke>, [ array, of, arguments] )

This object compiles directly into the catalog and its function is invoked as the first part of enforcing a catalog. It is replaced by whatever it returns, similar to string interpolation. The catalog looks like the JSON hash below.

The password key is replaced with the results of the vault_lookup::lookup invocation, and then the content key is replaced with the results of the inline_epp invocation. Puppet can then manage the contents of the file without the primary server ever knowing the secret.
$ jq '.resources[] | select(.type == "File" and .title == "/etc/secrets.conf")' catalog.json
{
  "type": "File",
  "title": "/etc/secrets.conf",
  "parameters": {
    "ensure": "file",
    "owner": "root",
    "group": "root",
    "mode": "0600",
    "content": {
      "__ptype": "Deferred",
      "name": "inline_epp",
      "arguments": [
        "PASSWORD=$password\n",
        {
          "password": {
            "__ptype": "Deferred",
            "name": "vault_lookup::lookup",
            "arguments": ["secret/test", "https://vault.docker:8200"]
          }
        }
      ]
    },
    "backup": false
  }
}
Note:

When using deferred functions, take note of the following:

  • If an agent is applying a cached catalog, the Deferred function is still called at application time, and the value returned at that time is the value that is used.
  • It is the responsibility of the function to handle edge cases such as providing default or cached values in cases where a remote store is unavailable.
  • Deferred supports only the Puppet function API for Ruby.
  • If a function called on the agent side does not return Sensitive, you can wrap the value returned by Deferred in a Sensitive type if a sensitive value is desired. For example: $d = Sensitive(Deferred("myupcase", ["example value"]))
  • Deferred functions can only use types that are built into Puppet (for example String). They cannot use types from modules like stdlib because Puppet does not plugin-sync these types to the agent.
  • Do not use the Deferred object as a variable in a string. When compiled, these variables are interpolated, so the stringified version of the object would be passed to the agent, instead of the object itself.