|
| 1 | +Getting your Puppet Ducks in a Row |
| 2 | +=== |
| 3 | +A conversation that comes up frequently is if the Puppet Programming Language is declarative or not. This is usually the topic when someone has been fighting with how order of evaluation works and have |
| 4 | +been beaten by what sometimes may seem as random behavior. In this post I want to explain how |
| 5 | +Puppet works and try to straighten out some of the misconceptions. |
| 6 | + |
| 7 | +First, lets get the terminology right (or this will remain confusing). It is common to refer to "parse order" instead of "evaluation order" and the use of "parse order" as a term is deeply rooted in the Puppet community - this is unfortunate as it is quite misleading. A language is first parsed and then evaluated, and as you will see, almost all of the peculiarities occur during evaluation. |
| 8 | + |
| 9 | +### "Parse Order" |
| 10 | + |
| 11 | +Parse Order is the order in which Puppet reads puppet manifests (`.pp`) from disk, turns them into tokens and checks their grammar. The result is something that can be evaluated (technically an Abstract Syntax Tree (AST)). The order in which this is done is actually of minor importance from a user perspective, you really do not need to think about how an expression such as `$a = 1 + 2` becomes an AST. |
| 12 | + |
| 13 | +OTOH, if you think about "parse order" as "the order the files are parsed", but this order is |
| 14 | +also of minor importance. Puppet starts with the `site.pp` file (or possibly the `code` setting in the configuration), then asking external services (such as the ENC) for additional things that are not included in the logic that is loaded from the `site.pp`. In versions from 3.5.0 the manifest setting can also refer to a directory of `.pp` files (preferred over using the now deprecated `import` statement). |
| 15 | + |
| 16 | +At this point (after having started and after having parsed the initial manifest(s), Puppet first matches the information about the node making a request for a catalog with available node definitions, and selects the first matching node definition. At this point Puppet has the notion of: |
| 17 | + |
| 18 | +* node - a mapping of the node the request is for. |
| 19 | +* a set of classes to include and possibly parameters to set that it got from external sources. |
| 20 | +* parsed content in the form of one or several ASTs (one per file that was initially parsed) |
| 21 | + |
| 22 | +Evaluation now starts of the puppet logic (evaluation of the ASTs). This evaluation order is imperative - lines in the logic are executed in the order they are written. Classes and Defines are defined as a consequence, but they are not evaluated (i.e. their bodies of code are just associated with the respective name and set aside for later "lazy" evaluation). |
| 23 | + |
| 24 | +### Definition and Declaration |
| 25 | + |
| 26 | +In computer science these term as used as follows: |
| 27 | + |
| 28 | +* Declaration - introduces a named entity and possibly its type, but it does not fully define |
| 29 | + the entity (its value, functionality, etc.) |
| 30 | +* Definition - binds a full definition to a name (possibly declared somewhere). A definition is what |
| 31 | + givens a variable a value, or defines the body of code for a function. |
| 32 | + |
| 33 | +A user defined resource type is defined in puppet using a `define` expression. E.g something like |
| 34 | +this: |
| 35 | + |
| 36 | + define mytype($some_parameter) { |
| 37 | + # body of definition |
| 38 | + } |
| 39 | + |
| 40 | +A host class is defined in puppet using the class expression. E.g. something like this: |
| 41 | + |
| 42 | + class ourapp { |
| 43 | + # body of class definition |
| 44 | + } |
| 45 | + |
| 46 | +However, if we at a point after that resource type definition or class definition have been made |
| 47 | +try to ask if `mytype` or `ourapp` is defined by using the function `defined`, we will be told that it is not! This is because the implementor of the function `defined` used the word in a very ambiguous manner - **the defined function actually answers "is it in the catalog?", not "do you know what a mytype is?"**. |
| 48 | + |
| 49 | +The terminology is further muddled by the fact that the result of a resource expression is computed in two steps - the instruction is queued, and later evaluated. Thus, there is a period of time |
| 50 | +when it is defined, but what it defines does not yet exist (a kind of recorded desire / partial evaluation). The `defined` function will however return true for resources that are in the queue |
| 51 | + |
| 52 | + mytype { '/tmp/foo': ...} |
| 53 | + notice defined(Mytype['tmp/foo']) # true |
| 54 | + |
| 55 | +When this is evaluated, a declaration of a mytype resource is made in the catalog being built. The |
| 56 | +actual resource '/tmp/foo' is "on its way to be evaluated" and the `defined` function returns `true` since it is "in the catalog" (only not quite yet). |
| 57 | + |
| 58 | +Read on, to learn more, or skip to the example if you want something concrete and then come back and read about "Order of Evaluation". |
| 59 | + |
| 60 | +### Order of Evaluation |
| 61 | + |
| 62 | +In order for a class to be evaluated, it must be included in the computation via a call to `include`, or by being instantiated via the resource instantiation expression. (In comparison to a classic Object Oriented programming language `include` is the same as creating a new instance of the class). Also note that instances of Puppet classes are singletons (a class can only be instantiated once in one catalog). |
| 63 | + |
| 64 | +If something is not included, then nothing that it in turn defines is visible. Originally, there was no "instantiate class as a resource" expression, only the `include` function call - and the idea was that you could include the class as many times you wanted (since there can only be one instance, the |
| 65 | +include call only repeats the desire to include that single instance). You always included a class before attempting to use it. Now, with parameterized classes, this does not work because classes are singletons and can thus not be instantiated with different sets of parameters (that would require having more than one instance). Unfortunately puppet cannot handle this even if the parameters are identical, or if it does not have any parameters at all - it sees this as an attempt of creating a second (illegal) instance of the class. |
| 66 | + |
| 67 | +When something includes a class (or uses the resource instantiation expression to do the same), the class is auto-loaded; this means that puppet maps the name to a file location, parses the content, and expects to find the class with a matching name. When it has found the class, this class is evaluated (its body of code is evaluated). |
| 68 | + |
| 69 | +The result of the evaluation is a catalog - the catalog contains resources and edges and is declarative. The catalog is transported to the agent, which applies the catalog. The order resources are applied is determined by their dependencies (and their containment, use of anchor pattern, or the `contain` function, and settings (apply in random, or by source order, etc.). No evaluation of any puppet logic takes place at this point (at least not in the current version of Puppet) - on the agent the evaluation is done by the providers operating on the resource in the order that is determined by the catalog application logic running on the agent. |
| 70 | + |
| 71 | +**The duality of this; a mostly imperative, but sometimes lazy production of a catalog and a declarative catalog application is something that confuses many users.** |
| 72 | + |
| 73 | +As an analog; if you are writing a web service in PHP, the PHP logic runs on the web server and produces HTML which is sent to the browser. The browser interprets the HTML (which consists of declarative markup) and makes decision what to render where and in which order the rendering |
| 74 | +will take place (images load in the background, some elements must be rendered first because their size is needed to position other elements etc.). Compared to Puppet; the imperative PHP backend corresponds to the master computing a catalog in a mostly imperative fashion, and an agent's application of the declarative catalog corresponds to the web browser's rendering of HTML. |
| 75 | + |
| 76 | +Up to this point, the business of "doing things in a particular order" is actually quite clear. |
| 77 | +What still remains to be explained is the order in which the bodies of classes and user defined types are evaluated, as well as when relationships and queries are evaluated. |
| 78 | + |
| 79 | +### Producing the Catalog |
| 80 | + |
| 81 | +The production of the catalog is handled by what is currently known as the "Puppet Compiler". This is again a misnomer, it is not a compiler in the sense that other computer languages have a compiler that translates the source text to machine code (or some intermediate form like Java Byte Code). It does however compile in the sense that it is assembling something (a catalog) out of many pieces of information (resources). Going forward (Puppet 4x) you will see us referring to **Catalog Builder** instead of Compiler - who knows, one day we may have an actual compiler (to machine code) that compiles the instructions that builds the catalog. Even if we do not, for anyone that has used a compiler it is not intuitive that the compiler runs the program, which is what the current Puppet Compiler does. |
| 82 | + |
| 83 | +When puppet evaluates the AST, it does this imperatively - `$a = $b + $c`, will immediately look up the value of $b, then $c, then add them, and then assign that value to `$a`. The evaluation will use the values assigned to `$b` and `$c` at the time the assignment expression is evaluated. There is nothing "lazy" going on here - it is not waiting for `$b` or `$c` to get a value that will be produced elsewhere at some later point in time. |
| 84 | + |
| 85 | +Some instructions have side effects - i.e. something that changes the state of something external to the function. An operation like `+` is a pure function - it takes two values and adds them, once this is done there is no memory of that having taken place (unless the result is used in yet another expression, etc. until it is assigned to some variable (a side effect). |
| 86 | + |
| 87 | +The operations that have an effect on the catalog are evaluated for the sole purpose of their side effect. The `include` function tells the catalog builder about our desire to have a particular class included in the catalog. A *resource expression* tells the catalog builder about our desire to have a particular resource applied by the agent, a dependency formed between resources again tells the catalog builder about our desire that one resource should be applied before/after another. While the instructions that cause the side effects are immediate, the side effects may be recorded for later action. This is the case for most operations that involve building a catalog. |
| 88 | + |
| 89 | +In other words, **expressions that perform catalog operations are (mostly) lazy** |
| 90 | + |
| 91 | +* An `include` will evaluate the body of a class (since classes are singletons this happens only |
| 92 | + once). The fact that we have instantiated the class is recorded in the catalog - a class is a |
| 93 | + container of resources, and the class instance is fully evaluated and it exists as a container, but |
| 94 | + it does not yet contained the actual resources. In fact, it only contains *instructions* |
| 95 | + (i.e. our desire to have a particular resource with particular parameter values to be applied on |
| 96 | + the agent). |
| 97 | +* A class included via what looks like a resource expression i.e. `class { name: }` behaves |
| 98 | + like the `include` function wrt. evaluation order. |
| 99 | +* A dependency between two (or a chain of) resources is also *instructions* at this point. |
| 100 | +* A query (i.e. space-ship expressions) are *instructions* to find and realize resources. |
| 101 | + |
| 102 | +The catalog building reaches a point where it starts processing. Processing begins by processing the queued up instructions to evaluate resources. Since a resource may be of user defined type, and it in turn may include other classes, the processing of resources gets interrupted and those classes are evaluated (this typically adds additional resource instructions to the queue). This continues until all instructions about what to place in the catalog have been evaluated (and nothing new was added). |
| 103 | +The lazy evaluation of the catalog building instructions are done in the order they were added to the catalog with the exception of queries and relations which are done at the very end. |
| 104 | + |
| 105 | + |
| 106 | +### How many different Orders are there? |
| 107 | + |
| 108 | +The different orders are: |
| 109 | + |
| 110 | +* **Parser Order** - a more or less insignificant term meaning the order in which text is translated |
| 111 | + into something the puppet runtime can act on. (If you have a problem with ordering, you are |
| 112 | + almost certainly not having a problem with parse order). |
| 113 | +* **Evaluation Order** - the order in which puppet logic is evaluated with the purpose of producing |
| 114 | + a catalog. |
| 115 | +* **Catalog Build Order** - in which order the catalog builder evaluates definitions. (If you are |
| 116 | + having problems with ordering, this is where things appears to be mysterious). |
| 117 | +* **Application Order** - the order in which the resources are applied on an agent (host). (If you |
| 118 | + are having ordering problems here, they are more apparent, "resource x" must come before |
| 119 | + "resource y", or something (like a file) that "resource y" needs will be missing). |
| 120 | + Solutions here are to use dependencies, the anchor pattern, or use the `contain` function.) |
| 121 | + |
| 122 | +### Please Make Puppet less Random! |
| 123 | + |
| 124 | +This is a request that pops up from time to time. Usually because someone has blown a fuse over |
| 125 | +a Catalog Build Order problem. As you have learned, the order is far from random. It is however, |
| 126 | +still quite complex to figure out the order, especially in a large system. Is there something we can do about this? |
| 127 | + |
| 128 | +Since the mechanisms in the language has been out there for a while, it is not an easy thing to change, there are many that rely on the current behavior - there are however many ways around the pitfalls that work well for people creating complex configurations - i.e. there are "best practices". There are also things that are impossible or difficult to achieve. |
| 129 | + |
| 130 | +There are many suggestions about how the language should change to be both more powerful and easier to understand, and several options are being considered to help with the mysterious Catalog Build |
| 131 | +Order and the constraints it imposes. These options are: |
| 132 | + |
| 133 | +* Being able to include a resource multiple times if they are identical (or that they augment each |
| 134 | + other). |
| 135 | +* Make a class include taking place before a class has been instantiated as a resource to be |
| 136 | + considered identical (since the include did not specify any parameters it can be considered as |
| 137 | + a desire of lower precedence). (It is currently possible allowed to do the reverse). |
| 138 | + |
| 139 | +Another common request is to support decoupling between resources, sometimes referred to as "co-op", |
| 140 | +where there is a desire to include things "if they are present" (as oppose to someone explicitly including them). The current set of functions and language mechanisms makes this hard to achieve (due to Catalog Build Order being complex to reason about). |
| 141 | + |
| 142 | +Here the best bet is the ENC (for older versions), or the Node Classifier for newer puppet versions. Related to this is the topic of "data in modules" which in part deals with the overall composition of the system. The features around "data in modules" have not been settled while there are experimental things to play with - none of the existing proposals is a clear winner. |
| 143 | + |
| 144 | +I guess this was a long way of saying - we will get to it in time. What we have to do first (and what we are working on) is the semantics of evaluation and catalog construction. At this point, the new evaluator (that evaluates the AST) is available when using the --parser future flag in the just released 3.5.0. We have just started up the work on the new Catalog Builder where we will more clearly (with the goal of being more strict and deterministic) define the semantics of the catalog and the process that constructs it. We currently do not have "inversion of control" as a feature (i.e. by adding a module to the module path you also make its starting point included), but are well aware of this feature being much wanted (in conjunction with being able to compose data). |
| 145 | + |
| 146 | +What better way to end than with a couple of examples... |
| 147 | + |
| 148 | +### Getting Your Ducks in a Row |
| 149 | + |
| 150 | +Here is an example of a manifest containing a number of ducks. In which order will they appear? |
| 151 | + |
| 152 | + define duck($name) { |
| 153 | + notice "duck $name" |
| 154 | + include c |
| 155 | + } |
| 156 | + |
| 157 | + class c { |
| 158 | + notice 'in c' |
| 159 | + duck { 'duck0': name => 'mc scrooge' } |
| 160 | + } |
| 161 | + |
| 162 | + class a { |
| 163 | + notice 'in a' |
| 164 | + duck {'duck1': name => 'donald' } |
| 165 | + include b |
| 166 | + duck {'duck2': name => 'daisy' } |
| 167 | + } |
| 168 | + |
| 169 | + class b { |
| 170 | + notice 'in b' |
| 171 | + duck {'duck3': name => 'huey' } |
| 172 | + duck {'duck4': name => 'dewey' } |
| 173 | + duck {'duck5': name => 'louie' } |
| 174 | + } |
| 175 | + |
| 176 | + include a |
| 177 | + |
| 178 | +This is the output: |
| 179 | + |
| 180 | + Notice: Scope(Class[A]): in a |
| 181 | + Notice: Scope(Class[B]): in b |
| 182 | + Notice: Scope(Duck[duck1]): duck donald |
| 183 | + Notice: Scope(Class[C]): in c |
| 184 | + Notice: Scope(Duck[duck3]): duck huey |
| 185 | + Notice: Scope(Duck[duck4]): duck dewey |
| 186 | + Notice: Scope(Duck[duck5]): duck louie |
| 187 | + Notice: Scope(Duck[duck2]): duck daisy |
| 188 | + Notice: Scope(Duck[duck0]): duck mc scrooge |
| 189 | + |
| 190 | +(This manifest is found [in this gist][1] if you want to get it an play with it yourself). |
| 191 | + |
| 192 | +Here is a walk through: |
| 193 | +* class a is included and its body starts to evaluate |
| 194 | +* it placed duck1-donald in the catalog builder's queue |
| 195 | +* it includes class b and starts evaluating its body (before it evaluates duck2 - daisy) |
| 196 | +* class b places ducks 3-5 (the nephews) in the catalog builder queue |
| 197 | +* class a evaluation continues, and duck 2-daisy is now placed in the queue |
| 198 | +* the evaluation is now done, and the catalog builder starts executing the queue |
| 199 | +* duck2-donald is first, this logs is name, and class c is included |
| 200 | +* class c queues duck0-mc scrooge |
| 201 | +* catalog now processes the remaining queued ducks in order 3,4,5,2,0 |
| 202 | + |
| 203 | +[1]:https://gist.github.com/hlindberg/9975348 |
| 204 | + |
| 205 | +The order in which resources are processed may seem to be random, but now you know the actual rules. |
| 206 | + |
| 207 | +### Getting Parameters from Resources |
| 208 | + |
| 209 | +In the future parser/evaluator in Puppet 3.5.0 it is possible to access the evaluated parameter values of a resource. As you probably have guessed, using this feature is tricky because of the queuing of resource evaluation. There is currently no function that answers the question "is this resource evaluated" so you must be very sure that you only use this feature in a resource body where you know that it is queued after the resource you want to get a parameter from. |
| 210 | + |
| 211 | +As an example, if you want to get the name of a Duck, this is what you write: |
| 212 | + |
| 213 | + notice "Name of duck duck1 is ${Duck[duck1][name]}" |
| 214 | + |
| 215 | +In the example above, if you define `allducks` as a resource type like this: |
| 216 | + |
| 217 | + define allducks { |
| 218 | + Integer[0,5].each |$index| { |
| 219 | + $name = Duck["duck${index}"][name] |
| 220 | + notice "Name of duck duck${index} is ${$name}" |
| 221 | + } |
| 222 | + } |
| 223 | + |
| 224 | +and then add that resource last in class c, like this: |
| 225 | + |
| 226 | + class c { |
| 227 | + notice 'in c' |
| 228 | + duck { 'duck0': name => 'mc scrooge' } |
| 229 | + allducks {'all-the-ducks': } |
| 230 | + } |
| 231 | + |
| 232 | +the following output is produced: |
| 233 | + |
| 234 | + Notice: Scope(Class[A]): in a |
| 235 | + Notice: Scope(Class[B]): in b |
| 236 | + Notice: Scope(Duck[duck1]): duck donald |
| 237 | + Notice: Scope(Class[C]): in c |
| 238 | + Notice: Scope(Duck[duck3]): duck huey |
| 239 | + Notice: Scope(Duck[duck4]): duck dewey |
| 240 | + Notice: Scope(Duck[duck5]): duck louie |
| 241 | + Notice: Scope(Duck[duck2]): duck daisy |
| 242 | + Notice: Scope(Duck[duck0]): duck mc scrooge |
| 243 | + Notice: Scope(Allducks[all-the-ducks]): Name of duck duck0 is mc scrooge} |
| 244 | + Notice: Scope(Allducks[all-the-ducks]): Name of duck duck1 is donald} |
| 245 | + Notice: Scope(Allducks[all-the-ducks]): Name of duck duck2 is daisy} |
| 246 | + Notice: Scope(Allducks[all-the-ducks]): Name of duck duck3 is huey} |
| 247 | + Notice: Scope(Allducks[all-the-ducks]): Name of duck duck4 is dewey} |
| 248 | + Notice: Scope(Allducks[all-the-ducks]): Name of duck duck5 is louie} |
| 249 | + |
| 250 | +### Summary |
| 251 | + |
| 252 | +In this (very long) post, I tried to explain "how puppet really works", and while the order in which puppet takes action may seem mysterious or random at first, it is actually defined and deterministic - albeit quite intuitive when reading the puppet logic at face value. |
| 253 | + |
0 commit comments