Ansible的幂等性(Idempotency)是指在多次执行相同任务时,结果不会改变。这是通过模块的设计和任务状态的管理实现的。具体来说,Ansible的幂等性主要通过以下几个方面实现:

状态检查

每个Ansible模块在执行操作前,首先检查目标系统的当前状态。如果目标系统已经处于期望状态,模块将不会执行任何改变。这避免了重复执行相同的操作。

- name: Ensure package is installed
  yum:
    name: nginx
    state: present

在这个例子中,yum模块会检查nginx包是否已经安装,如果已经安装,则不会重新安装。

改变检测

#简单示例
result = dict(
    changed=False,
    original_message='',
    message=''
)

if not is_installed:
    # 执行安装操作
    install_package()
    result['changed'] = True

如果包没有安装,模块会执行安装操作并将changed设置为True,否则changed保持为False

声明式配置

Ansible的任务是声明式的,即描述目标状态,而不是逐步指示如何实现该状态。这种方法使得任务本身具有幂等性,因为每次执行都会检查并设置为期望状态,而不是每次都执行相同的命令。

- name: Ensure nginx configuration file is present
  template:
    src: templates/nginx.conf.j2
    dest: /etc/nginx/nginx.conf

在这个例子中,template模块会检查模板文件是否已经正确地存在于目标位置,如果文件没有变化,则不会覆盖已有文件。

幂等性检查机制

Ansible模块通常会包含具体的幂等性检查机制,例如比较文件内容、配置参数等,以确定是否需要进行操作。

#简单示例代码
current_content = read_file(destination)
new_content = generate_content_from_template(template_path)

if current_content != new_content:
    write_file(destination, new_content)
    result['changed'] = True

这个示例代码中,模块会比较目标文件的当前内容和模板生成的新内容,只有在内容不同时才会进行写入操作,并将changed设置为True

file模块是如何实现幂等性的

代码来源于:[自动化]浅聊ansible的幂等 - 一介布衣·GZ - 博客园 (cnblogs.com)


vim /usr/lib/python2.7/site-packages/ansible/modules/files/file.py
.....
def get_state(path):
    ''' Find out current state '''

    b_path = to_bytes(path, errors='surrogate_or_strict')
    try:
        if os.path.lexists(b_path): # 如果文件存在返回file,文件不存在返回absent
            if os.path.islink(b_path):
                return 'link'
            elif os.path.isdir(b_path):
                return 'directory'
            elif os.stat(b_path).st_nlink > 1:
                return 'hard'

            # could be many other things, but defaulting to file
            return 'file' 

        return 'absent'
    except OSError as e:
        if e.errno == errno.ENOENT:  # It may already have been removed
            return 'absent'
        else:
            raise


def ensure_absent(path):
    b_path = to_bytes(path, errors='surrogate_or_strict')
    prev_state = get_state(b_path) # 获取文件的状态
    result = {}

    if prev_state != 'absent': # 当prev_state='directory' or 'file' 为真
        diff = initial_diff(path, 'absent', prev_state)

        if not module.check_mode:
            if prev_state == 'directory': # 如果prev_state='directory', 则删除目录
                try:
                    shutil.rmtree(b_path, ignore_errors=False)
                except Exception as e:
                    raise AnsibleModuleError(results={'msg': "rmtree failed: %s" % to_native(e)})
            else:
                try:
                    os.unlink(b_path) # 如果prev_state='file', 则删除文件
                except OSError as e:
                    if e.errno != errno.ENOENT:  # It may already have been removed
                        raise AnsibleModuleError(results={'msg': "unlinking failed: %s " % to_native(e),
                                                          'path': path})

        result.update({'path': path, 'changed': True, 'diff': diff, 'state': 'absent'}) # 删除文件成功,动作有改变,changed=True
    else:
        result.update({'path': path, 'changed': False, 'state': 'absent'}) # 如果prev_state='absent', 动作没有改变,changed=False, 实现多次操作执行不会有任何改变。

    return result


def main():

    global module

    module = AnsibleModule(
        argument_spec=dict(
            state=dict(type='str', choices=['absent', 'directory', 'file', 'hard', 'link', 'touch']),
            path=dict(type='path', required=True, aliases=['dest', 'name']),
            _original_basename=dict(type='str'),  # Internal use only, for recursive ops
            recurse=dict(type='bool', default=False),
            force=dict(type='bool', default=False),  # Note: Should not be in file_common_args in future
            follow=dict(type='bool', default=True),  # Note: Different default than file_common_args
            _diff_peek=dict(type='bool'),  # Internal use only, for internal checks in the action plugins
            src=dict(type='path'),  # Note: Should not be in file_common_args in future
            modification_time=dict(type='str'),
            modification_time_format=dict(type='str', default='%Y%m%d%H%M.%S'),
            access_time=dict(type='str'),
            access_time_format=dict(type='str', default='%Y%m%d%H%M.%S'),
        ),
        add_file_common_args=True,
        supports_check_mode=True,
    )

    # When we rewrite basic.py, we will do something similar to this on instantiating an AnsibleModule
    sys.excepthook = _ansible_excepthook
    additional_parameter_handling(module.params)
    params = module.params

    state = params['state']
    recurse = params['recurse']
    force = params['force']
    follow = params['follow']
    path = params['path']
    src = params['src']

    timestamps = {}
    timestamps['modification_time'] = keep_backward_compatibility_on_timestamps(params['modification_time'], state)
    timestamps['modification_time_format'] = params['modification_time_format']
    timestamps['access_time'] = keep_backward_compatibility_on_timestamps(params['access_time'], state)
    timestamps['access_time_format'] = params['access_time_format']

    # short-circuit for diff_peek
    if params['_diff_peek'] is not None:
        appears_binary = execute_diff_peek(to_bytes(path, errors='surrogate_or_strict'))
        module.exit_json(path=path, changed=False, appears_binary=appears_binary)

    if state == 'file':
        result = ensure_file_attributes(path, follow, timestamps)
    elif state == 'directory':
        result = ensure_directory(path, follow, recurse, timestamps)
    elif state == 'link':
        result = ensure_symlink(path, src, follow, force, timestamps)
    elif state == 'hard':
        result = ensure_hardlink(path, src, follow, force, timestamps)
    elif state == 'touch':
        result = execute_touch(path, follow, timestamps)
    elif state == 'absent': 
        result = ensure_absent(path) # 执行删除文件时,调用方法 def ensure_absent

    module.exit_json(**result)


if __name__ == '__main__':
    main()

非幂等模块

虽然说Ansible是具有幂等性,但根据我们以上所说其是通过模块的幂等性来实现的,在Ansible中也有非幂等性模块,比如:

  • command 和 shell 模块: 这两个模块用于在目标主机上运行任意命令。由于这些命令的执行结果可能每次都不同(例如,产生不同的输出、改变系统状态等),因此通常是非幂等的。

  • script 模块: 该模块用于在目标主机上运行本地脚本。脚本的执行可能会导致每次执行时产生不同的结果,因此也是非幂等的。

  • raw 模块: 该模块用于在目标主机上运行原始的SSH命令,与commandshell模块类似,其结果可能每次执行都不同,因此也是非幂等的。

  • git 模块(在某些情况下): 虽然git模块通常是幂等的,但在某些情况下,如拉取最新的代码或分支切换时,如果代码库发生变化,可能会导致非幂等行为。

  • synchronize 模块: 该模块用于在本地和远程主机之间同步文件,类似于rsync。如果源文件不断变化,可能会导致非幂等行为。

  • yum 模块(在某些情况下): 在执行诸如清理缓存或更新所有包的操作时,可能会导致非幂等行为。

以上几个模块是但在实际使用中,可以通过结合其他模块或添加额外的状态检查来尽量减少非幂等行为的影响。例如,可以在执行commandshell模块之前,先使用stat模块检查文件是否存在,或者使用when条件来控制任务的执行条件。