SaltStack: How to Execute a State Only Once for MySQL Replication Setup


5 views

When configuring MySQL master-slave replication through SaltStack, a common requirement is executing certain states just once during initial setup. The specific pain point comes when you need to retrieve the master's binary log position (SHOW MASTER STATUS) exactly once to configure the slave.

Consider this typical state declaration:

get_master_status:
  module.run:
    - name: mysql.query
    - query: SHOW MASTER STATUS

This runs every highstate, potentially overwriting your replication coordinates. The fundamental issue is that SaltStack states are designed to be idempotent - they naturally want to enforce the declared state on every run.

Here are three battle-tested approaches:

1. The File Flag Pattern

A common solution using Salt's built-in capabilities:

# Create flag file if doesn't exist
create_flag:
  file.touch:
    - name: /etc/mysql/master_status_grabbed
    - unless: test -f /etc/mysql/master_status_grabbed

# Only run if flag is missing
get_master_status:
  module.run:
    - name: mysql.query
    - query: SHOW MASTER STATUS
    - unless: test -f /etc/mysql/master_status_grabbed

2. Custom Execution Module

For cleaner implementation, create _modules/onetime.py:

def execute(name, fun, **kwargs):
    flag_path = f"/var/lib/salt/onetime_flags/{name}"
    if __salt__['file.file_exists'](flag_path):
        return {'name': name, 'result': True, 'comment': 'Already executed'}
    
    ret = __salt__[fun](**kwargs)
    if ret.get('result', False):
        __salt__['file.touch'](flag_path)
    return ret

Usage in SLS:

get_master_status:
  onetime.execute:
    - fun: mysql.query
    - query: SHOW MASTER STATUS

3. Orchestrate with Reactor

For complex scenarios, use the reactor system:

# reactor/master_status.sls
execute_once:
  local.state.apply:
    - tgt: 'mysql-master'
    - arg:
      - mysql.show_master_status
    - kwarg:
      - queue: True

# reactor/init.sls
react_master_ready:
  reactor:
    - 'salt/minion/*/start':
      - /srv/reactor/master_status.sls

When implementing one-time execution in production:

  • Store flags in persistent storage (/var/lib/salt preferred over /tmp)
  • Include cleanup logic in your teardown procedures
  • Consider using Salt's mine system for master status if you need periodic updates
  • For complex multi-machine sequences, orchestrate with state.sls

When automating MySQL replication setup with SaltStack, we often need to capture the master status exactly once during initial configuration. The naive approach of running SHOW MASTER STATUS in every highstate execution creates inconsistency in replication parameters.

Many users try these problematic patterns:

# Problematic approach 1 - Runs every time
get_master_status:
  mysql.query:
    - query: SHOW MASTER STATUS

# Problematic approach 2 - Creates failed states
/tmp/master_status_done:
  file.missing: []
  
get_master_status:
  mysql.query:
    - query: SHOW MASTER STATUS
    - require:
      - file: /tmp/master_status_done

Solution 1: Custom State Module with Flagging

Create a custom state module that implements atomic flag checking:

# salt/_states/myonce.py
import os

def query(name, query, flag_file=None):
    if flag_file and os.path.exists(flag_file):
        return {'name': name, 'result': True, 'comment': 'Already executed'}
    
    # Your actual query logic here
    result = __salt__['mysql.query'](query)
    
    if flag_file:
        with open(flag_file, 'w') as f:
            f.write('executed')
    
    return {'name': name, 'result': True, 'changes': {'data': result}}

Solution 2: Using Orchestration with Pillar

For cluster-wide one-time operations, consider orchestration:

# salt/orchestrate/mysql_replication.sls
configure_replication:
  salt.state:
    - tgt: 'mysql-master-*'
    - sls:
      - mysql.master_setup
    - pillar:
      first_run: True

For enterprise deployments, combine several techniques:

# salt/mysql/replication/init.sls
{% if salt['pillar.get']('mysql:initial_replication') %}

capture_master_status:
  myonce.query:
    - name: SHOW MASTER STATUS
    - flag: /etc/mysql/master_status_captured
    - require:
      - sls: mysql.server_installed

configure_slave:
  cmd.run:
    - name: |
        mysql -e "CHANGE MASTER TO
        MASTER_HOST='{{ pillar.mysql.master_host }}',
        MASTER_LOG_FILE='{{ salt.file.read('/etc/mysql/master_log_file') }}',
        MASTER_LOG_POS={{ salt.file.read('/etc/mysql/master_log_pos') }}"
    - unless: mysql -e "SHOW SLAVE STATUS" | grep -q "Waiting for master"
{% endif %}
  • Using Salt's event.send to trigger one-time operations
  • Leveraging reactors for cluster-wide coordination
  • Storing execution state in external systems (Vault, etcd)