Ever wondered how Icinga 2 manages all those variables, and how it knows which one to use? In this blog post, we will explore all the different variable scopes in Icinga 2, and by the end, you will know what this mysterious error message means when you see it in your logs:
critical/config: Error: Error while evaluating expression: Tried to access undefined script variable 'myVar'
What are variable scopes?
In Icinga 2 DSL, variable scopes are used to determine the visibility and lifetime of variables. They define where a variable can be found and how long it exists in the context of its usage. Understanding variable scopes is crucial for writing effective Icinga 2 configurations and avoiding common pitfalls like the error message mentioned above.
Variable Scopes
Icinga 2 has three different variable scopes: global
, this
, and local
. Each scope has its own rules for defining and accessing variables, and you can think of them as series of stacked boxes, with each box having its own set of variables. The global
scope is the most permissive, and allows you to access variables from anywhere in the configuration. The this
scope is more restrictive, and you can access variables only within the context of the current object. Finally, the local
scope is the most restrictive, allowing access to variables only within the block or function where they are defined. Let’s take a closer look at each of these scopes and how they work in practice.
Global Scope
The global scope is the highest level of variable scope in Icinga 2. Variables defined in the global scope are accessible from anywhere (is equivalent to Go’s universe
scope). There’s exactly one global
scope in Icinga 2, and is typically used for constants or settings that need to be shared across the entire configuration. To define a global variable, you can use the globals
keyword explicitly, like this:
globals.myGlobalVar = "This is a global variable"
The globals
keyword is optional, but it is a good practice to use it for clarity. If you omit it, the variable will still be treated as a global variable, but Icinga 2 will also emit a warning in the logs, indicating that it is being defined as a global variable without the globals
keyword. This is to help you avoid confusion and ensure that your configuration is clear.
warning/config: Global variable 'myGlobalVar' has been set implicitly via 'myGlobalVar = ...' in icinga2.conf: 32:3-32:31. Please set it explicitly via 'globals.myGlobalVar = ...' instead.
However, Icinga 2 will freeze the globals
scope at some point during the config loading process, which means that you cannot define new global variables after that. From that point on, any attempt to modify the globals
scope will result in an error message like this:
[2025-06-02 11:05:08 +0200] critical/config: Error: Namespace is read-only and must not be modified. Location: in icinga2.conf: 79:3-79:38 icinga2.conf(77): return x * y; icinga2.conf(78): } icinga2.conf(79): globals.helloworld = "Hello, World!" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Similar to how you define global variables, you can also define global functions. These functions can be called from anywhere in your configuration, making them useful for reusable logic or operations that need to be performed across multiple files. To define a global function, you can optionally use the globals
keyword followed by the function name and its implementation:
globals.fib = function(x) { if (x <= 1) { return x; } return fib(x - 1) + fib(x - 2); }
You can also define the function without the globals
keyword, but again, it is a good practice to use it for clarity.
function fib(x) { ... }
Constants
Constants are similar to variables, but they are immutable and cannot be changed once defined. In Icinga 2, all constants reside in the global scope, and you can define them using the const
keyword. For example, you can define a constant like this:
const MAX_RETRIES = 5
This defines a constant named MAX_RETRIES
with a value of 5. Once defined, you cannot change the value of this constant, and any attempt to do so will result in an error message like this:
[2025-06-04 11:50:37 +0200] critical/config: Error: Constant must not be modified. Location: in icinga2.conf: 56:3-56:18 icinga2.conf(56): MAX_RETRIES = 50 ^^^^^^^^^^^^^^^^
Just remember that global variables are accessible from anywhere, but they should be used sparingly to avoid clutter and confusion in your configuration. They are best suited for constants and configuration settings, just like how Icinga 2 uses it to define some system information, like the globals.System.Math
variable, which contains the built-in mathematical functions that you can use in your config.
The this
Scope
The this
scope is a special variable scope that refers to the current object or context in which the code is being evaluated. If you’re familiar with object-oriented programming, you can think of this
as the equivalent of self in Python or this in JavaScript. It allows you to access properties and methods (if any) of the current object. In Icinga 2, the this
scope usually refers to some object/apply
context, such as a host or service object. When you set a variable with the this
scope, you must also make sure that the variable you’re trying to access is allowed in that context. For instance, if you try to set some variable in an object Host ...
context, you can only access variables that are defined in the Host
object, such as name, address, etc., otherwise you will get an error like this:
[2025-06-02 11:40:49 +0200] critical/config: Error: Attribute 'my_variable' does not exist. Location: in icinga2.conf: 77:3-77:49 icinga2.conf(77): this.my_variable = "Illegal 'this' Scope usage" ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
When you want to set any valid object attribute within an object block, you don’t even need to use the this
scope, as it is implied except for some special cases. For example, if you want to set the display_name
attribute of a host object, you can simply do it like this:
object Host "myhost" { this.display_name = "My Host" // <<-- This is valid, but not necessary display_name = "My Host" // <<-- You usually just do it like this }
The this
scope is particularly useful when you use some poorly chosen variable names that might conflict with the built-in attributes of the object. In such cases, using this
can help you avoid confusion and ensure that you’re accessing the correct variable. However, it’s generally recommended to use descriptive and unique variable names to avoid such conflicts in the first place. We’ll cover this in more detail in the next section.
Local Scope
The local scope is the most restrictive variable scope in Icinga 2. They are typically used for temporary values or intermediate calculations that do not need to be shared outside their context. To define a local variable, you can either use the locals
or var
keyword, like this anywhere in your config file:
locals.myLocalVar = "This is a local variable" //... or like this var myLocalVar = "This is also a local variable"
Local variables exist only for the duration of the block or function in which they are defined. Once the block or function execution is complete, the local variable is no longer accessible. This makes local variables ideal for temporary values that do not need to be retained beyond their immediate context. Sometimes, you might want to use a local variable defined in a parent local scope. In such cases, you must explicitly import the variable using the use
keyword into the child local scope. For example:
object Host "myhost" use(myLocalVar) { ... // some host configuration // If you want to use the local variable `myLocalVar` defined in the parent scope, // you must explicitly import it into this block like how we did in the `use(myLocalVar)` line above. vars.myVar = myLocalVar }
Likewise, you can also define local functions that are only accessible within the block or function where they are defined. The syntax for defining a local function is similar to that of a local variable, they just differ in the type of value you assign to them. For example:
locals.gcd = (x, y) => { if (y == 0) { return x; } return gcd(y, x % y); }
In order to make use of that function in some lower scope/block, you must also explicitly import just like with local variables:
object Host "myhost" use(gcd) { // Now you can use the local function `gcd` defined in the parent scope log(LogInformation, "config", "GCD of 48 and 18 is: " + gcd(48, 18)) }
When running icinga2 daemon -C
, you will see the output of the log statement in the console, indicating that the local function was successfully imported and used in the child scope.
... information/config: GCD of 48 and 18 is: 6
If you define a variable or function via the locals
or var
keyword outside any block, it will be local to the config file where it is defined, and any other scopes defined in that file can access it by importing it as described above. However, since Icinga 2 evaluates the configs within a single file in a top-down manner, you must ensure that the local variable or function is defined before it is used via the use
keyword. Other config files can also access such a local variable, however, you should never do that, as the config files are evaluated sorted alphabetically, and the local variable might not be defined yet when the other config file tries to access it. This will just bring you some unnecessary headaches and confusion, so it’s best to avoid using local variables across different config files.
Now, that you know the differences between the three scopes in Icinga 2, I’ll briefly explain how Icinga 2 performs actual variable resolution when evaluating the expressions in the next section.
Variable Resolution in Icinga 2
When Icinga 2 evaluates an expression, it follows a specific order to resolve variables based on their scopes. The order of resolution is as follows:
- Local Scope: Icinga 2 first checks the local scope for the variable. If it finds a variable with the same name, it uses that value and stops searching further.
- This Scope: If the variable is not found in the local scope, Icinga 2 checks the
this
scope. If it finds a variable with the same name in thethis
scope, it aborts the lookup and uses the value of that variable. - Global Scope: Only if the variable is not found in either the local or
this
scope, Icinga 2 checks the global scope. If it finds a variable with the same name in the global scope, then it uses that value and is done searching.
You see, Icinga 2 resolves variables in a hierarchical manner, starting from the local scope and moving up to the global scope. That’s why you should be careful when defining variables with the same name in different scopes, as it can lead to unexpected behavior and confusion. For instance, if you’ve a configuration like this:
object Host "myhost" { //... some host configuration var display_name = "Don't do this" // <<-- This is a bad variable name display_name = "It's just a bad idea" }
In this case, as we’ve learned, the var
keyword defines a local variable named display_name
, nothing wrong with that so far. However, the Host
object (the this
scope) also has a built-in attribute named display_name
, which you can set to whatever you want. So, when you try to set the display_name
variable in the above example, Icinga 2 will resolve it to the just defined local variable, which isn’t what you want. Fortunately, if you really want to use such a conflicting variable name, you can just be more explicit about which variable you want to override like this:
object Host "myhost" { //... some host configuration var display_name = "Don't do this" // <<-- Still a bad variable name this.display_name = "It's just a bad idea" // <<-- Tell Icinga 2 that you want to set the `display_name` attribute of the Host object }
In general, it’s a good practice to use descriptive and unique variable names to avoid such conflicts. Using the explicit globals
or locals
scope specifiers to access a variable does work correctly, but Icinga 2 won’t trigger any errors as you might expect if the variable isn’t defined in the used scope. Due to how the Indexer works, accessing a variable globals.some.thing
won’t throw an error if globals.some
or globals.some.thing
is not defined, but will just resolve to null
instead.
... { var myLocalVar = globals.some.thing }
Here, I didn’t define globals.some.thing
anywhere, but instead of throwing an error, the Indexer will just yield null
for the globals.some.thing
part, and the myLocalVar
will be set to that value.
Conclusion
That pretty much sums up the variable scopes in Icinga 2 and how they work. In my next blog post, I will cover how you can create your own custom variable scopes and how they can be useful in certain scenarios. But for now, can you guess what this Icinga 2 error log means 🙃? Come on, it’s not that hard 😉!
critical/config: Error: Error while evaluating expression: Tried to access undefined script variable 'gcd'
Right, you knew it! The error message indicates that Icinga 2 tried to resolve the variable gcd
in all the three scopes, but was unable to. Next time you see this error, make sure that you follow the rules of variable scopes and that the variable is defined in the correct scope before you try to access it. If you still have questions or need further clarification, feel free to reach out in the comments or in the community forum.