Backbeat Software
Photo by Pablò on Unsplash

Configuring Prometheus targets with SaltStack

How to configure static Prometheus targets using Salt.

Glynn Forrest
Thursday, April 30, 2020

Prometheus is a pull-based monitoring server. At a high level, you configure it to read metrics from a series of HTTP address, also known as scrape targets. These scrape targets are hosted by various exporters as well as your own applications.

At Backbeat we use SaltStack and Consul to tell it about these targets. In this post we’ll go through some SaltStack techniques, and discuss Consul in a future post.

Static configs

The most basic scrape targets are hard coded with the static_configs option:

scrape_configs:
  - job_name: 'node_exporter'
    static_configs:
      - targets:
        - '10.10.10.1:9100'
        - '10.10.10.2:9100'
        - '10.10.10.3:9100'

This tells Prometheus to scrape three IP addresses on port 9100 (the default node_exporter port) with a job name of node_exporter.

Set static config targets using a custom execution module

What if we want to scrape targets that change constantly, e.g. web servers in an autoscaling group? With Salt jinja templating, it’d be ideal to have something like this:

{% set web_ips = salt['get_web_ips']() %}
scrape_configs:
  - job_name: 'node_exporter'
    static_configs:
      - targets:
      {% for ip in web_ips %}
        - '{{ip}}:9100'
      {% endfor %}

The tricky part, of course, is implementing the get_web_ips salt function. Let’s start by adding a custom execution module that returns the hard coded IP addresses. In your salt directory, create _modules/custom.py:

def web_ips():
    return [
      '10.10.10.1',
      '10.10.10.2',
      '10.10.10.3',
    ]

Sync the new module to the Prometheus minion with saltutil.sync_modules, then update the Prometheus config template:

+ {% set web_ips = salt['custom.web_ips']() %}
- {% set web_ips = salt['get_web_ips']() %}
  scrape_configs:
    - job_name: 'node_exporter'

Success! The target IP addresses will be read from the custom module.

Making the module dynamic with Salt Mine

How do we change the list of IPs returned? For example, with two web minions:

web1.example.com - 10.10.10.1
web2.example.com - 10.10.10.2

Or four web minions:

web1.example.com - 10.10.10.1
web2.example.com - 10.10.10.2
web3.example.com - 10.10.10.3
web4.example.com - 10.10.10.4

Getting this information is easy on the salt master, assuming the eth1 interface:

salt 'web*' grains.get ip4_interfaces:eth1:0

web1.example.com:
    10.10.10.1
web2.example.com:
    10.10.10.2
web3.example.com:
    10.10.10.3
web4.example.com:
    10.10.10.4

Minions can’t access information about other minions however, so if we configure Prometheus on the stats.example.com minion we won’t be able to do this.

Instead, we need to publish this information to the Salt Mine for other minions to access.

On each web minion, create /etc/salt/minion.d/mine_functions.conf:

# Run mine functions every 60 minutes and when the minion starts
mine_interval: 60

mine_functions:
  ip_address:
    - mine_function: grains.get
    - 'ip4_interfaces:eth1:0'

This makes each minion’s IP address available in the ip_address mine entry. We can access the mine from any minion:

salt stats.example.com mine.get web\* ip_address

stats.example.com:
    ----------
    web1.example.com:
        10.10.10.1
    web2.example.com:
        10.10.10.2

Updating our module is simple:

  def web_ips():
+     return __salt__['mine.get']('web*', 'ip_address').values()
-     return [
-       '10.10.10.1',
-       '10.10.10.2',
-       '10.10.10.3',
-     ]

Now the returned IP address will stay up to date. Perfect!

Instance labelling

With this configuration, the Prometheus metrics will look something like this:

Humans brains aren’t designed to handle IP addresses, so let’s tweak the configuration to set the instance label to the name of the minion instead.

First, override the instance label with a test value:

      static_configs:
        - targets:
          {% for ip in web_ips %}
          - '{{ip}}:9100'
          {% endfor %}
+         labels:
+           instance: test

It changed the instance label, but we’ve combined two time series into one! We need a different label for each instance, something the simple labels: option of static_configs can’t manage. Instead, create a separate static_configs entry for every minion:

      static_configs:
+       {% for ip in web_ips %}
+       - targets:
+         - '{{ip}}:9100'
+         labels:
+           instance: '{{ip}}'
+       {% endfor %}
-       - targets:
-         {% for ip in web_ips %}
-         - '{{ip}}:9100'
-         {% endfor %}
-         labels:
-           instance: test

Then tweak the custom module to return both the minion name and IP address:

  def web_ips():
+     return __salt__['mine.get']('web*', 'ip_address')
-     return __salt__['mine.get']('web*', 'ip_address').values()
      static_configs:
+       {% for minion, ip in web_ips.items() %}
-       {% for ip in web_ips %}
        - targets:
          - '{{ip}}:9100'
          labels:
+           instance: '{{minion}}'
-           instance: '{{ip}}'
        {% endfor %}

Much better! Here’s the final Prometheus configuration:

{% set web_ips = salt['get_web_ips']() %}
scrape_configs:
  - job_name: 'node_exporter'
    static_configs:
      {% for minion, ip in web_ips.items() %}
      - targets:
        - '{{ip}}:9100'
        labels:
          instance: '{{minion}}'
      {% endfor %}

Tips and gotchas

  • Make sure to update the Prometheus configuration regularly. With Alertmanager active, you’ll receive alerts for minions that have been deliberately destroyed until the Prometheus configuration is updated again.
  • Depending on how minions are destroyed, the Salt Mine can sometimes return stale data. This means you may see an IP address of an old minion from custom.web_ips(), which will be added as a failing Prometheus target. See issues #11389 and #21986 for more information and workarounds.
  • Adding a Reactor that updates the Prometheus configuration on minion create and destroy events can be a good solution.

Conclusion

With a tiny bit of custom code, we’ve configured Prometheus to scrape a variable amount of minions using the Salt Mine. Mining the IPs of other minions works well for other tools too, such as load balancers and firewalls.

In a future post, we’ll use the consul_sd_configs option and Consul to discover scrape targets. We’ll also cover relabel_configs, a much more powerful technique for changing labels to fit our needs.

Need any help getting your Prometheus stack running? Send us an email!

More from the blog

Configuring Prometheus targets with Consul cover image

Configuring Prometheus targets with Consul

How to configure dynamic Prometheus targets using Consul.


Glynn Forrest
Sunday, May 31, 2020

Rotating pet servers with SaltStack cover image

Rotating pet servers with SaltStack

How to rotate a machine with minimal downtime.


Glynn Forrest
Saturday, August 31, 2019

Building a SaltStack development machine cover image

Building a SaltStack development machine

Using Vagrant and Salt to work on Salt’s codebase.


Glynn Forrest
Wednesday, July 31, 2019