You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Ansible.builtin.copy校验和一致却报告文件变更问题求助

问题:Ansible copy任务重复报告文件变更,但SHA256校验一致

我在自动化拉取OS镜像并添加cloud-init的user-data文件,流程是将原始镜像复制到工作目录修改,再复制到输出目录。但最后一步copy任务每次都报告文件变更,用sha256sum ubuntu-2204.customized.img*检查备份文件与新文件,校验和完全匹配。

相关任务代码:

- name: Copying modified image to output directory
  ansible.builtin.copy:
    src: "{{ working.path }}/{{ pi_image }}.img"
    dest: "output/{{ pi_image }}.customized.img"
    mode: preserve
    remote_src: true
    backup: true

完整代码参考

groups/all/raspberry_pi.yml

pi_username: james
pi_password: password
pi_hostname: management01
pi_public_key_path: /home/james/.ssh/id_rsa.pub

pi_supported_images:
  ubuntu-2204:
    download_url: https://cdimage.ubuntu.com/releases/22.04/release/ubuntu-22.04.5-preinstalled-server-arm64+raspi.img.xz
    download_checksum: sha256:fd7687c5c9422a6c7ba4717c227bf6473fe4e0c954d5a9f664201dcecc63e822

  rocky-9:
    download_url: https://dl.rockylinux.org/pub/sig/9/altarch/aarch64/images/RockyLinuxRpi_9-latest.img.xz
    download_checksum: sha256:1ba3c7649279ef1b3b48cd2caa3a8b0ce4483accc095ba6f88661f55f8f91138

pi_image: ubuntu-2204

Playbook

- name: Build Raspberry Pi Server ISO
  hosts: localhost
  become: true
  force_handlers: true
  tasks:
    - name: "Fetching {{ pi_image }}"
      ansible.builtin.get_url:
        url: "{{ pi_supported_images[pi_image].download_url }}"
        checksum: "{{ pi_supported_images[pi_image].download_checksum }}"
        dest: "output/{{ pi_image }}.img.xz"
        mode: u=r,g=r,o=r

    - name: Generating user-data file
      ansible.builtin.template:
        src: templates/user-data.jinja
        dest: "output/{{ pi_image }}-user-data"
        mode: u=r,g=r,o=r

    - name: Making working directory
      ansible.builtin.file:
        state: directory
        path: "working/{{ pi_image }}-{{ ansible_date_time.epoch }}"
        mode: u=xrw,g=xr,o=xr
      register: working
      notify: Remove working directory

    - name: Copying compressed image to working directory
      ansible.builtin.copy:
        src: "output/{{ pi_image }}.img.xz"
        dest: "{{ working.path }}/{{ pi_image }}.img.xz"
        mode: u=r,g=r,o=r

    - name: Extracting compressed image
      ansible.builtin.command:
        argv:
          - xz
          - --keep
          - --decompress
          - "{{ working.path }}/{{ pi_image }}.img.xz"
      register: xz_result
      changed_when: xz_result.rc == 0

    - name: Copying extracted image to output directory
      ansible.builtin.copy:
        src: "{{ working.path }}/{{ pi_image }}.img"
        dest: "output/{{ pi_image }}.img"
        mode: preserve
        remote_src: true

    - name: Setting up loopback device
      ansible.builtin.command:
        argv:
          - losetup
          - --show
          - --partscan
          - --find
          - "{{ working.path }}/{{ pi_image }}.img"
      register: losetup_attach
      changed_when: losetup_attach.rc == 0
      notify: Remove loopback device

    - name: Mounting boot partition
      ansible.posix.mount:
        src: "{{ losetup_attach.stdout }}p1"
        path: "{{ working.path }}/{{ pi_image }}-boot-partition"
        state: ephemeral
        fstype: vfat
      notify: Remove mount

    - name: Placing user-data file
      ansible.builtin.command:
        argv:
          - "cp"
          - "--archive"
          - "output/{{ pi_image }}-user-data"
          - "{{ working.path }}/{{ pi_image }}-boot-partition/user-data"
      register: cp_user_data
      changed_when: cp_user_data.rc == 0

    - name: Copying modified image to output directory
      ansible.builtin.copy:
        src: "{{ working.path }}/{{ pi_image }}.img"
        dest: "output/{{ pi_image }}.customized.img"
        mode: u=r,g=r,o=r
        backup: true

  handlers:
    - name: Remove mount
      ansible.posix.mount:
        path: "{{ working.path }}/{{ pi_image }}-boot-partition"
        state: unmounted

    - name: Remove loopback device
      ansible.builtin.command: "losetup --detach {{ losetup_attach.stdout }}"
      register: losetup_detach
      changed_when: losetup_detach.rc == 0

    - name: Remove working directory
      ansible.builtin.debug:
        msg: "Removing working directory..."
    #   ansible.builtin.file:
    #     path: "{{ working.path }}"
    #     state: absent

templates/user-data.jinja

#cloud-config

chpasswd:
  expire: false
  users:
  - name: {{ pi_username }}
    password: {{ pi_password }}
    type: text

{% if pi_hostname %}
hostname: {{ pi_hostname }}
{% endif %}

users:
  - name: {{ pi_username }}
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
    ssh-authorized-keys:
      - {{ lookup('ansible.builtin.file', pi_public_key_path, errors='strict') }}

package_update: true
package_upgrade: true

排查原因

Ansible的copy模块判断文件是否变更,不只是校验文件内容,还会检查文件元数据(权限、时间戳、所有者/组)。即使内容一致,只要元数据有差异,就会标记为变更。结合你的场景,可能的触发点:

  1. 权限强制修改:最后一步copy任务指定了mode: u=r,g=r,o=r,但工作目录内的镜像文件权限与该配置不符,每次复制都会强制修改权限,触发变更标记。
  2. 时间戳差异:工作目录的镜像文件是解压或修改后生成的,时间戳与输出目录已存在的文件不同,Ansible默认会通过时间戳判断是否需要更新。
  3. 备份操作干扰:开启backup: true后,每次复制都会生成备份文件,部分场景下Ansible会因备份操作本身标记为变更。

解决方案

方案1:保持权限一致

将最后一步copy任务的mode改回preserve,让目标文件继承源文件的权限,避免强制修改权限触发变更:

- name: Copying modified image to output directory
  ansible.builtin.copy:
    src: "{{ working.path }}/{{ pi_image }}.img"
    dest: "output/{{ pi_image }}.customized.img"
    mode: preserve
    remote_src: true
    backup: true

方案2:强制校验文件内容

使用checksum参数,指定用SHA256校验判断文件是否需要更新,忽略元数据差异:

- name: Copying modified image to output directory
  ansible.builtin.copy:
    src: "{{ working.path }}/{{ pi_image }}.img"
    dest: "output/{{ pi_image }}.customized.img"
    mode: u=r,g=r,o=r
    remote_src: true
    backup: true
    checksum: "{{ lookup('ansible.builtin.sha256', working.path + '/' + pi_image + '.img') }}"

方案3:改用系统命令复制

如果不想依赖Ansible的判断逻辑,直接用cp --archive --update命令,仅在源文件更新时复制:

- name: Copying modified image to output directory
  ansible.builtin.command:
    argv:
      - cp
      - --archive
      - --update
      - "{{ working.path }}/{{ pi_image }}.img"
      - "output/{{ pi_image }}.customized.img"
  register: cp_result
  changed_when: cp_result.rc == 0 and cp_result.stdout != ''

额外优化:清理工作目录

启用Remove working directory handler的实际删除逻辑,避免每次运行生成大量冗余工作目录:

- name: Remove working directory
  ansible.builtin.file:
    path: "{{ working.path }}"
    state: absent

内容的提问来源于stack exchange,提问作者James Ayres

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.14 23:37:03