Ansible OEL 8 DevOps · OEL 8 · Beginner

AnsibleYour First Playbook

Walk through writing, running, and reading the output of a real playbook that installs and configures Apache httpd on an OEL 8 host — line by line, with every concept anchored to what you actually see on screen.

A 4-task playbook that, on a fresh OEL 8 host, will: install httpd, drop a custom index.html, configure the firewall, and start the service. After running it once, running it again should show zero changes — that's idempotency in action.

ansible-playbook site.yml -i hosts PLAY [oel8 hosts] defines targets & tasks TASK [Gathering facts] ok TASK [Install httpd package] chg TASK [Start httpd service] chg PLAY RECAP — ok=3 changed=2 unreachable=0 failed=0
BASH — project layout
# create the directory structure
mkdir -p first-playbook/{templates,files}
cd first-playbook

# inventory file
cat > hosts.ini <<EOF
[oel8]
192.168.56.11

[oel8:vars]
ansible_user=deploy
EOF

# minimal ansible.cfg so we don't have to pass -i every time
cat > ansible.cfg <<EOF
[defaults]
inventory = ./hosts.ini
host_key_checking = False
stdout_callback = yaml

[privilege_escalation]
become = True
EOF

ls
# ansible.cfg  files  hosts.ini  templates
YAML — site.yml
---
- name: Configure a basic Apache web server on OEL 8
  hosts: oel8
  become: true
  vars:
    httpd_port: 80
    welcome_message: "Hello from Ansible on {{{{ inventory_hostname }}}}"

  tasks:
    - name: Install Apache HTTPD
      ansible.builtin.dnf:
        name: httpd
        state: present

    - name: Render the welcome page from template
      ansible.builtin.copy:
        dest: /var/www/html/index.html
        content: |
          <html>
            <head><title>It works</title></head>
            <body>
              <h1>{{{{ welcome_message }}}}</h1>
              <p>This page was deployed by Ansible at {{{{ ansible_date_time.iso8601 }}}}.</p>
            </body>
          </html>
        owner: apache
        group: apache
        mode: "0644"

    - name: Open the HTTP port in firewalld
      ansible.posix.firewalld:
        service: http
        permanent: true
        state: enabled
        immediate: true

    - name: Start and enable httpd at boot
      ansible.builtin.systemd:
        name: httpd
        state: started
        enabled: true
💡 Note: The double braces inside the template ({{{{ welcome_message }}}}) are Jinja2 placeholders. Ansible swaps them with variable values just before executing the task. We cover Jinja2 properly on page 13 of this series.
BASH — syntax check
ansible-playbook --syntax-check site.yml
# expected output:
#   playbook: site.yml
BASH — dry run
ansible-playbook site.yml --check --diff
# --check  : simulate, don't actually change anything
# --diff   : show the line-level diff for any file Ansible would change

Read the output carefully. Every task should be reported as either ok (already in the desired state) or changed (would be modified). If anything says failed or unreachable, fix it before running for real.

BASH — first run
ansible-playbook site.yml

# typical output (truncated, real time ~ 8s)
PLAY [Configure a basic Apache web server on OEL 8] **********

TASK [Gathering Facts] ***************************************
ok: [192.168.56.11]

TASK [Install Apache HTTPD] **********************************
changed: [192.168.56.11]

TASK [Render the welcome page from template] *****************
changed: [192.168.56.11]

TASK [Open the HTTP port in firewalld] ***********************
changed: [192.168.56.11]

TASK [Start and enable httpd at boot] ************************
changed: [192.168.56.11]

PLAY RECAP ***************************************************
192.168.56.11 : ok=5  changed=4  unreachable=0  failed=0  skipped=0
BASH — verify it works
curl -s http://192.168.56.11/
# <html>
#   <head><title>It works</title></head>
#   <body>
#     <h1>Hello from Ansible on db1.example.com</h1>
#     <p>This page was deployed by Ansible at 2026-04-30T08:42:11Z.</p>
#   </body>
# </html>
BASH — second run shows zero changes
ansible-playbook site.yml

PLAY RECAP ***************************************************
192.168.56.11 : ok=5  changed=0  unreachable=0  failed=0  skipped=0
#                      ^^^^^^^^^
#                      every task: ok, no change made
✅ Tip: That zero-changes second run is the whole point of declarative automation. You can run this playbook every 5 minutes via cron or in a CI pipeline and nothing breaks — Ansible only acts when reality drifts from the desired state.
  • The 4-step develop loop: --syntax-check--check --diff → run → re-run to verify idempotency
  • How variables (vars:) are referenced in tasks via Jinja2
  • The difference between changed (something modified) and ok (already correct)
  • That a playbook + inventory + ansible.cfg is the minimum viable Ansible project