Introduction
Several related one-off commands are easier to repeat and review when stored in a playbook. A useful playbook describes stable desired state and produces observable results on every intended target.
You will write a small YAML playbook using the file and copy modules, apply it to two SSH nodes, and verify both the created files and a zero-change repeat run. This lab introduces plays, tasks and idempotency without variables, loops or roles.
Write a Playbook as YAML
In this step, you will combine the file and copy modules you already practiced into a reusable playbook.
Enter the project in the Terminal:
cd /home/labex/project
A playbook is a YAML list of plays. Each play binds a host pattern to an ordered task list. Each task names a module and its arguments. name labels help humans read output; they do not create configuration by themselves.
YAML uses indentation to nest data. Use spaces, not tabs. A leading - starts a list item, and a colon introduces a key's value. Here, tasks contains two module tasks. Quoted modes such as "0750" remain permission strings. gather_facts: false skips automatic fact collection because these fixed-state tasks do not consume facts.
Create the file with a quoted here-document, as practiced earlier. Copy through the final EOF line:
cat > workspace.yml <<'EOF'
- name: Prepare managed workspaces
hosts: all
gather_facts: false
tasks:
- name: Create the workspace directory
ansible.builtin.file:
path: /home/automation/workspace
state: directory
mode: "0750"
- name: Write the workspace notice
ansible.builtin.copy:
dest: /home/automation/workspace/README.txt
content: "Workspace managed by Ansible\n"
mode: "0640"
EOF
The first task declares a directory. The second uses copy's content argument to provide literal text directly, instead of reading a controller src file. The double-quoted YAML string interprets \n as a final newline. Fixed content makes a repeat run stable; do not add a changing timestamp.
The play uses hosts: all, meaning both supplied SSH nodes. Ordinary tasks run as automation, so their home directory is writable without become.
ansible-playbook executes playbook files. First use --syntax-check to validate structure without applying tasks:
ansible-playbook workspace.yml --syntax-check
Expect a playbook identification line and no parser error. Syntax validation alone does not prove remote connectivity or correct final files.
Preview the play's host selection using --list-hosts:
ansible-playbook workspace.yml --list-hosts
The play must list web1 and web2. An empty list is not a usable two-host playbook.
Apply the Desired State
In this step, you will run the playbook and inspect its actual remote results.
Execute the file from the project so its configuration and inventory are selected:
ansible-playbook workspace.yml
Ansible prints each play and task name, then a PLAY RECAP. On a fresh environment, the two tasks should change each host:
web1 : ok=2 changed=2 unreachable=0 failed=0 ...
web2 : ok=2 changed=2 unreachable=0 failed=0 ...
Spacing and host order may differ. ok counts successful tasks, changed counts tasks that needed changes, and unreachable/failed should both be zero. A successful task name alone does not prove the required file bytes or permissions.
Read each remote notice using the ad hoc command module:
ansible all -m ansible.builtin.command -a 'cat /home/automation/workspace/README.txt'
Both stdout values should show Workspace managed by Ansible. The read-only command query can still report CHANGED; the playbook's file/copy results describe the declared state. The managed directory has mode 0750, the notice mode 0640, and both are owned by automation.
Verify Idempotent Convergence
In this step, you will repeat the playbook and confirm that matching state requires no extra changes.
Idempotency means applying the same desired state repeatedly does not produce unnecessary changes after convergence. Run the same playbook again:
ansible-playbook workspace.yml
Expect a recap for each target with ok=2, changed=0, unreachable=0 and failed=0. Both files remain present and correct. A changing timestamp or an unconditional general command would undermine this stable result.
--check requests a preview: supported modules predict changes without applying them. Both file and copy support this workflow. Preview the converged playbook:
ansible-playbook workspace.yml --check
Expect zero changes for both hosts again. A preview does not replace the first real execution or inspection of actual state. Here it confirms that the already-created workspaces match the declarations.
Keep the playbook for future reuse. Do not suppress changes with changed_when or force tasks out of check mode to manufacture a clean recap; the result should reflect actual matching state.
Summary
You expressed host selection and ordered module tasks in YAML, checked syntax and target scope, and applied the playbook to two real managed nodes. Actual file state matched the requirements, a second real run made no extra changes, and read-only check mode found no pending changes.



