Ansible Lesson 37 of 42

Ansible for SAP, In Depth: HANA System Replication, NetWeaver, Kernel Patching & Landscape Automation

Ansible for SAP, In Depth — HANA System Replication, NetWeaver, Kernel Patching and Landscape Automation

In a nutshell

SAP is the software that runs the finance, supply chain, manufacturing, and HR of a large share of the world’s biggest companies. The team that keeps it alive — installing the database, tuning the operating system, patching, and making sure the whole thing survives a datacentre failure — is called SAP basis. Their work is governed by hundreds of vendor rules (called SAP Notes), and a single missed setting can turn a routine Saturday-night patch into a six-hour outage that shows up in Monday’s board meeting.

Ansible turns that pile of rules into code. Instead of a basis engineer working down a five-page checklist by hand at 2 a.m., a playbook applies every setting the same way, every time, on every host. Think of it as the pre-flight checklist for a jumbo jet: pilots do not trust memory for something this consequential — they read the list and tick every item, every single flight. Ansible is that checklist, executed by a machine that never skips a line and never gets tired.

The trick that keeps you sane is to stand on the vendor’s shoulders. Red Hat and the community publish certified collections (redhat.sap_install, community.sap_libs) that already encode those hundreds of SAP Notes and wrap the SAP installers. You do not hand-write the kernel tuning; you call the role that knows it. The rest of this lesson shows how a real landscape — HANA, NetWeaver, HA clustering, patching, transports — is built and operated from that foundation.

Level: Advanced · Time: ~40 min read

SAP is the production landscape that scares most automation teams. The runbooks read like vendor documentation written in 2002, the OS prerequisites are five pages long, and a single mis-tuned kernel parameter manifests as a six-hour HANA recovery. Ansible, used carefully, makes it tractable: every SAP Note becomes a role, every SID a host group, every transport a job template, every patch wave a workflow.

This lesson is the specialist guide to automating the SAP basis function with Ansible: preparing OS hosts to SAP standards, installing HANA scale-up and scale-out, configuring HANA system replication (HSR) with Pacemaker, deploying NetWeaver ABAP/Java instances, running kernel and SPS patches, importing transports, and running the whole landscape from AAP without becoming the bottleneck for every basis change request.

We will be opinionated. The Red Hat-supported sap_install and sap_hana_install collections are the path that keeps you out of trouble; we will use them. The Linux Pacemaker stack is the cluster that SAP itself documents; we will use it. The vendor-specific stacks (HP Serviceguard, Veritas) exist but do not integrate with Ansible cleanly; we will not cover them.

Position in the curriculum. Tier 1–4 fluency required, plus the Tier 5 compliance, DR, and migrations lessons. SAP environments are usually all three combined: regulated (SOX/PCI), high-availability/DR-critical, and constantly migrating between hardware refreshes and HANA SPS upgrades.


Before you start — prerequisites and outcomes

This is one of the most advanced lessons in the course. It assumes you are already comfortable with the Ansible fundamentals and can read a role without translating every line.

You should already be able to:

After this lesson you will be able to:

  1. Lay out a repository that separates OS-prep, database, application, and orchestration into small playbooks that are cheap to rerun.
  2. Prepare a RHEL host to SAP-Note spec using the certified redhat.sap_install preconfigure roles — instead of transcribing Notes by hand.
  3. Install HANA scale-up and wire two nodes into HANA System Replication (HSR) fronted by a Pacemaker cluster with STONITH.
  4. Deploy NetWeaver ASCS/ERS as a clustered pair and scale out PAS/AAS dialog instances idempotently.
  5. Patch the SAP kernel one instance at a time — ERS before ASCS, dialogs one-by-one — without taking the landscape down.
  6. Drive transports, run synthetic RFC smoke tests, and reason correctly about idempotency and check-mode for long, one-way, stateful installs.

If you are missing the prerequisites, read those lessons first — this one moves fast and does not re-teach them.


What “SAP automation” really covers

SAP basis is a small team supporting a sprawl of systems. The work splits into four buckets:

  1. OS preparation per SAP Notes: kernel parameters, swap, transparent huge pages, NUMA topology, filesystem layouts, NTP/chrony, SELinux rules, network MTUs. Driven by SAP Notes (e.g., 2009879 for RHEL 7, 2235581 for RHEL 8, 3108316 for RHEL 9). Every Note has a hundred line items.
  2. Database installation and patching: HANA installs, SPS upgrades, kernel patching, parameter tuning, HSR setup, backup/recovery configuration.
  3. Application server installation and patching: NetWeaver ABAP and Java stacks, kernel patches, transport imports, profile parameter management.
  4. Landscape orchestration: refresh of QA from production, system copy automation, transport imports across the landscape (DEV → QA → PRD), service start/stop coordination during patching.

Ansible handles all four; the key is to use the SAP-supported collections rather than reinventing the wheel. Three collections matter:

These collections wrap sapinst, hdblcm, pcs, and the various SAP CLI tools, and bake in the conditionals from the SAP Notes. The collections are kept current with SAP releases by Red Hat — using them is how you avoid building a 5,000-line in-house “sap_role” that breaks every quarter.

How to read the collection landscape (beginner orientation). The names above are the logical building blocks this lesson uses; on a real machine you will meet a slightly different, evolving set. It helps to hold two categories in your head:

You need to… Reach for What it is
Prepare the OS, install HANA/NetWeaver, build HA clusters redhat.sap_install (roles like sap_general_preconfigure, sap_hana_preconfigure, sap_hana_install, sap_swpm, sap_ha_pacemaker_cluster) Red Hat’s certified, supported collection — the consolidated “do the heavy lifting” roles
Talk to a running SAP system (RFC, hdbsql, sapcontrol, apply a Note, run a task list) community.sap_libs (modules like sap_pyrfc, sap_hdbsql, sap_control_exec, sap_snote, sap_task_list_execute, sapcar_extract) Community modules for day-2 operations against an installed system
Provision cloud/VM infrastructure under SAP your cloud collection (amazon.aws, azure.azcollection, google.cloud) + ansible.posix, community.general The bottom layer the SAP roles sit on

The exact collection and role names shift release to release (Red Hat has been consolidating several older, separately-named collections into redhat.sap_install). The durable idea is the split above: certified roles do the install; community modules operate the running system. We revisit versioning discipline in Going deeper.


A representative SAP landscape

For concreteness, the rest of this lesson assumes a typical mid-size production landscape:

This landscape is what 80% of enterprise SAP shops look like; the Ansible patterns scale to scale-out HANA and to S/4HANA Cloud private edition with minor adjustments.


The SAP repository layout

sap-automation/
├── ansible.cfg
├── collections/requirements.yml      # redhat.sap_install,# redhat.sap_hana_install,# redhat.sap_management,# community.sap_install,# ansible.posix, community.general
├── inventory/
│   ├── prd/                           # production
│   │   └── hosts.yml                   #   hana_primary, hana_secondary, ascs, ers, pas, aas
│   ├── qa/                            # quality assurance
│   └── dev/                           # development
├── group_vars/
│   ├── all/
│   │   ├── sap_landscape.yml           # SID, instance numbers, DB connect strings
│   │   └── vault.yml                   # sap_installer_password, hana_master_pw, sapsys gid
│   ├── hana/
│   │   ├── sap_hana_install.yml
│   │   └── hana_hsr.yml
│   ├── netweaver/
│   │   ├── sap_install.yml
│   │   └── ascs_ers_cluster.yml
│   └── all_sap/
│       └── os_prep.yml                 # SAP Notes-driven kernel/sysctl/limits/THP
├── playbooks/
│   ├── 00-os-prep.yml
│   ├── 10-hana-install-primary.yml
│   ├── 11-hana-install-secondary.yml
│   ├── 12-hana-hsr-enable.yml
│   ├── 13-hana-pacemaker.yml
│   ├── 20-netweaver-ascs-install.yml
│   ├── 21-netweaver-ers-install.yml
│   ├── 22-netweaver-pas-install.yml
│   ├── 23-netweaver-aas-install.yml
│   ├── 30-kernel-patch.yml
│   ├── 31-hana-sps-patch.yml
│   ├── 40-system-refresh-prd-to-qa.yml
│   ├── 50-transport-import.yml
│   └── 99-decommission.yml
└── roles/
    ├── sap_os_prep/                    # wraps SAP Notes per RHEL major version
    ├── sap_storage_layout/
    ├── sap_users_groups/
    ├── sap_install_media_stage/
    ├── sap_hana_install_wrapper/       # wraps redhat.sap_hana_install with our defaults
    ├── sap_hana_hsr/
    ├── sap_pacemaker_cluster/
    ├── sap_netweaver_install_wrapper/
    ├── sap_kernel_patch/
    ├── sap_transport_import/
    ├── sap_validate/
    └── sap_evidence/

The split is deliberate: small playbooks per phase (so a rerun is cheap), wrapper roles around the upstream collections (so we can layer in our defaults), and shared roles for OS prep, storage and users (so DEV/QA/PRD are built from the same code).

Two naming ideas earn their keep here for a beginner: the numeric playbook prefixes (00, 10, 11…) encode the order a landscape is built in — you can literally read the build sequence off ls; and the _wrapper suffix flags “this role calls a vendor role but adds our house defaults,” so nobody edits the vendor role directly.


OS preparation per SAP Notes

Every SAP installation begins with OS preparation. The relevant SAP Notes for RHEL 9 are 3108316 (general), 2002167 (NetApp NFS for HANA), 2382421 (Linux kernel parameters), and 1771258 (Linux NUMA layout). Each Note is a list of conditionals; together they fill several pages.

Rather than transcribe Notes manually, use redhat.sap_install.sap_general_preconfigure (and its sibling roles) which encode the Notes for you:

# playbooks/00-os-prep.yml
---
- name: SAP OS preparation (per SAP Notes)
  hosts: all_sap
  become: true
  collections:
    - redhat.sap_install
  roles:
    - role: sap_general_preconfigure
      vars:
        sap_general_preconfigure_modify_etc_hosts: false   # we manage hosts via dnsmasq/cloud DNS
        sap_general_preconfigure_kernel_parameters_2382421: true
        sap_general_preconfigure_min_swap_space: 20480     # MB

    - role: sap_hana_preconfigure
      when: "'hana' in group_names"
      vars:
        sap_hana_preconfigure_kernel_parameters_NetApp: true
        sap_hana_preconfigure_thp: never
        sap_hana_preconfigure_numa_balancing: 0

    - role: sap_netweaver_preconfigure
      when: "'netweaver' in group_names"

Under the hood these roles set:

The tuned profiles are the most opinionated piece; they tune the entire kernel for SAP workloads. Run tuned-adm active after to verify.

Why this is the highest-leverage automation you own. These preconfigure roles are the one part of the stack that is genuinely idempotent and check-mode-friendly (see Going deeper). That means you can run them repeatedly, in --check mode, as a compliance probe — “is Note 2382421 still applied on all 40 hosts?” — not just as a one-time installer. When an auditor asks, the answer is a green playbook run, not a screenshot.


Storage layout

HANA has strict storage layout requirements. Every basis team has a story about a single mis-laid filesystem that took down a quarterly close. Use a dedicated role and lock the values down:

# roles/sap_storage_layout/tasks/main.yml
---
- name: Create HANA volume groups (LVM)
  community.general.lvg:
    vg: "vg_hana_{{ item.name }}"
    pvs: "{{ item.pvs }}"
  loop:
    - { name: "data",   pvs: "/dev/disk/by-id/{{ data_disk }}" }
    - { name: "log",    pvs: "/dev/disk/by-id/{{ log_disk }}" }
    - { name: "shared", pvs: "/dev/disk/by-id/{{ shared_disk }}" }

- name: Create HANA logical volumes
  community.general.lvol:
    vg: "vg_hana_{{ item.vg }}"
    lv: "lv_{{ item.name }}"
    size: "{{ item.size }}"
    state: present
  loop:
    - { vg: data,   name: data,   size: "{{ hana_data_size }}" }
    - { vg: log,    name: log,    size: "{{ hana_log_size }}" }
    - { vg: shared, name: shared, size: "{{ hana_shared_size }}" }

- name: Create HANA filesystems (XFS, 4K)
  community.general.filesystem:
    fstype: xfs
    dev: "/dev/mapper/vg_hana_{{ item.vg }}-lv_{{ item.name }}"
    opts: "-f -K -d agcount=64"
  loop: "{{ hana_lvs }}"

- name: Mount HANA filesystems
  ansible.posix.mount:
    src: "/dev/mapper/vg_hana_{{ item.vg }}-lv_{{ item.name }}"
    path: "{{ item.path }}"
    fstype: xfs
    opts: "noatime,inode64,nobarrier"
    state: mounted
  loop:
    - { vg: data,   name: data,   path: "/hana/data/{{ sid }}" }
    - { vg: log,    name: log,    path: "/hana/log/{{ sid }}" }
    - { vg: shared, name: shared, path: "/hana/shared" }

The mount options matter: inode64 for large filesystems, noatime for performance, nobarrier only when underlying storage has battery-backed write cache (NetApp, modern enterprise SAN — never for cloud EBS without io2 Block Express). Read SAP Note 1944799 before deviating.

For cloud SAP (AWS X1/X2, Azure M-series, GCP m2/m3), the corresponding native disk products replace the LVM steps but the mount options stay the same.


HANA scale-up install

redhat.sap_hana_install wraps hdblcm with the exact answer-file format SAP expects. The role accepts a structured set of vars; you do not edit hdblcm arguments directly.

# playbooks/10-hana-install-primary.yml
---
- name: Install HANA on primary node
  hosts: hana_primary
  become: true
  collections:
    - redhat.sap_hana_install
  vars:
    sap_hana_install_software_directory: /sapmedia/HANA_2_SPS07
    sap_hana_install_sid: "{{ sid }}"
    sap_hana_install_instance_number: "00"
    sap_hana_install_master_password: "{{ vault_hana_master_password }}"
    sap_hana_install_use_master_password_for_users: true
    sap_hana_install_system_usage: production
    sap_hana_install_apply_license: true
    sap_hana_install_license_path: /sapmedia/license/license-{{ sid }}.txt
    sap_hana_install_components:
      - server
      - client
      - studio
      - xs
  roles:
    - role: sap_install_media_stage      # ensures HANA media is on host (NFS/scratch)
    - role: sap_hana_install_wrapper      # invokes redhat.sap_hana_install role
    - role: sap_hana_post_install         # license, audit, parameters

The most frequent install bug is media that is not fully extracted; sap_install_media_stage should verify the SAR/EXE checksums and unpack to a known location before HANA install begins.

The post-install role applies parameters (global.ini, indexserver.ini) using redhat.sap_management.sap_hana_set_parameters. Common defaults set:


HANA System Replication (HSR)

HSR is the critical-path DR feature of HANA. It replicates redo logs from primary to secondary in three modes:

At a glance, so a beginner can choose without re-reading the paragraph:

Mode Commit waits for RPO (data loss) Performance cost Typical use
sync secondary persisted to disk 0 Highest Two datacentres on one campus; zero-data-loss mandated
syncmem secondary received into memory ≈ 0 Moderate Same-region HA pair — the common default
async local commit only (fire-and-forget) seconds Lowest Cross-region DR; accept some loss on a forced takeover

replicationMode (the table above) is only half the picture. operationMode is the second axis: logreplay (secondary continuously replays redo → fast takeover, the modern default, and what the register command below uses) versus the older delta_datashipping (slower to take over). You almost always want logreplay.

For two-DC same-region HSR, use SyncMem; for cross-region, use Async with a clear understanding that some transactions will be lost on a forced failover.

Ansible automates HSR setup once HANA is installed on both nodes:

# playbooks/12-hana-hsr-enable.yml
---
- name: Enable HSR primary
  hosts: hana_primary
  become: true
  tasks:
    - name: Enable system replication on primary
      ansible.builtin.command:
        cmd: |
          su - {{ sid_lc }}adm -c "
            hdbnsutil -sr_enable --name=DC1
          "
      register: enable
      changed_when: "'successfully enabled' in enable.stdout"

- name: Enable HSR secondary (after key copy)
  hosts: hana_secondary
  become: true
  tasks:
    - name: Stop HANA on secondary (must be down for register)
      ansible.builtin.command:
        cmd: su - {{ sid_lc }}adm -c "HDB stop"

    - name: Copy SSFS keys from primary to secondary
      ansible.posix.synchronize:
        src: "/usr/sap/{{ sid }}/SYS/global/security/rsecssfs/"
        dest: "/usr/sap/{{ sid }}/SYS/global/security/rsecssfs/"
      delegate_to: "{{ groups['hana_primary'][0] }}"

    - name: Register secondary with primary
      ansible.builtin.command:
        cmd: |
          su - {{ sid_lc }}adm -c "
            hdbnsutil -sr_register
              --remoteHost={{ hostvars[groups['hana_primary'][0]].ansible_host }}
              --remoteInstance={{ instance_number }}
              --replicationMode=syncmem
              --operationMode=logreplay
              --name=DC2
          "
      register: register

    - name: Start HANA on secondary
      ansible.builtin.command:
        cmd: su - {{ sid_lc }}adm -c "HDB start"

- name: Verify replication is in sync
  hosts: hana_primary
  become: true
  tasks:
    - name: Query HSR state
      ansible.builtin.command:
        cmd: |
          su - {{ sid_lc }}adm -c "
            python /usr/sap/{{ sid }}/HDB{{ instance_number }}/exe/python_support/systemReplicationStatus.py
          "
      register: hsr_state
      retries: 30
      delay: 10
      until: "'Replication mode' in hsr_state.stdout and 'OPERATION MODE' in hsr_state.stdout"

The systemReplicationStatus.py query is the canonical “is HSR healthy?” check; the role retries until the status line shows the secondary as ACTIVE. Wire this into a periodic redhat.sap_management.sap_hana_check_hsr_status job in AAP that runs every 5 minutes and pages on degradation.


Pacemaker cluster for HANA HSR

The official Linux HA cluster for HANA on RHEL is Pacemaker with the SAPHana and SAPHanaTopology resource agents. redhat.sap_management.sap_ha_pacemaker_cluster wraps the entire setup:

# playbooks/13-hana-pacemaker.yml
---
- name: HANA Pacemaker HA
  hosts: hana_cluster
  become: true
  collections:
    - redhat.sap_management
  vars:
    sap_ha_cluster_node_list:
      - "{{ groups['hana_primary'][0] }}"
      - "{{ groups['hana_secondary'][0] }}"
    sap_ha_cluster_authkey: "{{ vault_pcsd_password }}"
    sap_ha_cluster_hacluster_password: "{{ vault_hacluster_password }}"
    sap_ha_cluster_resource_stonith: fence_aws    # or fence_vmware_rest, fence_ipmilan
    sap_ha_cluster_resource_vip: "{{ hana_virtual_ip }}"
    sap_ha_cluster_resource_vip_secondary: "{{ hana_secondary_vip }}"
    sap_ha_cluster_sid: "{{ sid }}"
    sap_ha_cluster_instance_number: "{{ instance_number }}"
  roles:
    - sap_ha_install_pacemaker
    - sap_ha_pacemaker_cluster
    - sap_ha_install_hana_hsr_angi    # ANGI = Active Next Generation Implementation

The cluster:

The cluster takeover behaviour you actually care about:


NetWeaver ASCS/ERS HA

NetWeaver also needs HA. The ASCS instance (Application Server Central Services) and the ERS instance (Enqueue Replication Server) form a clustered pair. They share NFS-mounted directories (/sapmnt, /usr/sap/<SID>/ASCS<NN>).

# playbooks/20-netweaver-ascs-install.yml
- hosts: ascs
  become: true
  collections:
    - redhat.sap_install
  vars:
    sap_swpm_inifile_list:
      - PRD-NW-ASCS-INI
    sap_swpm_template_inifile: ascs.params.j2
  roles:
    - sap_install_media_stage
    - sap_install                 # wraps SWPM (sapinst)

For the cluster:

# playbooks/21-netweaver-cluster.yml
- hosts: ascs_ers_cluster
  become: true
  collections:
    - redhat.sap_management
  vars:
    sap_ha_cluster_resource_stonith: fence_vmware_rest
    sap_ha_cluster_resource_vip: "{{ ascs_vip }}"
    sap_ha_cluster_resource_vip_ers: "{{ ers_vip }}"
    sap_ha_cluster_sid: "{{ sid }}"
    sap_ha_cluster_ascs_instance_number: "00"
    sap_ha_cluster_ers_instance_number: "10"
  roles:
    - sap_ha_install_pacemaker
    - sap_ha_pacemaker_cluster_nw

The Pacemaker resources for NetWeaver:

Without ERS replication, an ASCS failover loses every uncommitted lock, which translates to user-visible “wait, I need to redo my entry” errors. ERS keeps the lock table mirrored.


Dialog instances (PAS, AAS)

The PAS (Primary Application Server) and AAS (Additional Application Server) are not clustered; they are scaled out behind the SAP Web Dispatcher or the load balancer. Ansible installs each instance idempotently:

# playbooks/22-netweaver-pas-install.yml
- hosts: pas
  become: true
  collections:
    - redhat.sap_install
  vars:
    sap_swpm_inifile_list:
      - PRD-NW-PAS-INI
  roles:
    - sap_install_media_stage
    - sap_install

# playbooks/23-netweaver-aas-install.yml
- hosts: aas
  become: true
  collections:
    - redhat.sap_install
  vars:
    sap_swpm_inifile_list:
      - PRD-NW-AAS-INI
  roles:
    - sap_install_media_stage
    - sap_install

The point is repeatability: when you scale out a fifth dialog instance during peak season, the playbook is the same one that built the first four. No “manual install with screenshots” runbook.


Kernel patching (SAP kernel, not Linux kernel)

The SAP kernel ships separately from the application stack. Patching is a quarterly-or-better activity. The pattern with Ansible:

# playbooks/30-kernel-patch.yml
- name: SAP kernel patch
  hosts: sap_landscape
  become: true
  serial: 1                 # one host at a time, controlled
  collections:
    - redhat.sap_management
  tasks:
    - import_role:
        name: sap_kernel_patch
      vars:
        sap_kernel_patch_target_kernel: 7.94    # canonical version
        sap_kernel_patch_media_dir: /sapmedia/kernel/7.94
        sap_kernel_patch_pre_check: true
        sap_kernel_patch_backup_old: true
        sap_kernel_patch_post_validate: true

The sap_kernel_patch role:

  1. Stops the instance gracefully (stopsap or systemd target).
  2. Backs up /sapmnt/<SID>/exe and /usr/sap/<SID>/SYS/exe.
  3. Extracts the new kernel SAR files via SAPCAR.
  4. Updates symlinks.
  5. Runs saproot.sh.
  6. Starts the instance.
  7. Validates with disp+work -V and a basic transaction (SE38 test program via SAP RFC if RFC creds available).

Patching is serial: 1 because you patch one instance at a time across the landscape — never simultaneously across an HA pair. For ASCS/ERS, you patch the ERS first (drains locks), failover, patch the ASCS, fail back. For PAS/AAS, you patch one dialog at a time so the load balancer drains and re-adds it cleanly.


Transport imports

SAP changes (development objects, configuration, customising) move across the landscape as transports: dev-side tp exports, QA-side imports, then PRD imports. Ansible can drive imports cleanly:

# playbooks/50-transport-import.yml
- hosts: ascs              # CI host runs from the Application Server side
  become_user: "{{ sid_lc }}adm"
  collections:
    - community.sap_install
  vars:
    transport_request: "PRDK900153"
    transport_target_system: PRD
  tasks:
    - name: Add request to import queue
      ansible.builtin.command:
        cmd: |
          tp addtobuffer {{ transport_request }} {{ transport_target_system }}
            -Dpf=/usr/sap/trans/bin/TP_DOMAIN_PRD.PFL
      register: add_buffer

    - name: Import the request
      ansible.builtin.command:
        cmd: |
          tp import {{ transport_request }} {{ transport_target_system }}
            client=100
            -Dpf=/usr/sap/trans/bin/TP_DOMAIN_PRD.PFL
            U126   # ignore inactive imports as needed
      register: tp_import
      failed_when: tp_import.rc not in [0, 4]   # 0 = clean, 4 = warnings

    - name: Persist tp logs
      ansible.builtin.fetch:
        src: "/usr/sap/trans/log/{{ transport_request }}.{{ transport_target_system }}"
        dest: "./tp-logs/"
        flat: true

The interesting AAP-level orchestration is the change ticket gate: a survey job template asks for a CHG ticket number, calls ServiceNow to verify the ticket is in “Implement” state, and then dispatches the transport import. The ITSM and ChatOps lesson covers this pattern in detail.


System refresh (PRD → QA)

System refresh is the periodic operation of “make QA look like PRD again.” It is high-risk and historically very manual. Ansible automates the whole flow:

  1. Pre-refresh export of QA-only data (test users, test customising) that you want to preserve.
  2. HANA backup on PRD → restore on QA via hdbsql and recoverSys.py.
  3. Post-refresh import of QA-only data.
  4. Customising adjustments for QA (e.g., disable email outputs, redirect printers, mask sensitive tables).
  5. Smoke test with predefined transactions.

The Ansible role for this is large but the structure is simple: it is just a long playbook with many tasks, each guarded by tags so a partial re-run is feasible. The single most important rule: the refresh must be idempotent on retry, because the first attempt almost always finds something the team forgot.


Validation: synthetic transactions, not just service checks

After every patch, install, or HSR operation, run a synthetic SAP transaction. The community.sap_install.sap_rfc_call module wraps PyRFC to call any RFC-enabled function module:

- name: Smoke test  RFC ping
  community.sap_install.sap_rfc_call:
    sap_host: "{{ ascs_vip }}"
    sap_sysnr: "00"
    sap_client: "100"
    sap_user: "{{ vault_smoke_test_user }}"
    sap_password: "{{ vault_smoke_test_password }}"
    function: "STFC_CONNECTION"
    parameters:
      REQUTEXT: "ansible-{{ ansible_date_time.epoch }}"
  register: rfc
  no_log: true

- name: Assert RFC works
  ansible.builtin.assert:
    that: rfc.return_value.ECHOTEXT == "ansible-{{ ansible_date_time.epoch }}"

STFC_CONNECTION is the canonical “round-trip” RFC; it proves the entire stack — from VIP to dispatcher to work process — is functional. After kernel patches, run this. After HSR takeover, run this. After every transport import, run this.

Module-name note (current collections). The example above uses the lesson’s community.sap_install.sap_rfc_call naming. On a current control node the community RFC module you will most often install is community.sap_libs.sap_pyrfc, which takes a connection: dict and a function:/parameters: pair. The Practice challenges below use the community.sap_libs names so you have both spellings side by side — check ansible-galaxy collection list and the module --doc for the exact names in your environment before you copy either.


SAP-on-Cloud specifics

Ansible is the same; cloud-specific roles change. AWS, Azure, and GCP each have a “SAP on cloud” reference architecture that Ansible automates:

Ansible roles wrap the cloud-specific provisioning steps; the SAP-specific roles (HANA install, NetWeaver, kernel patch) are unchanged. This is the pattern that makes Ansible a good fit: cloud-specific bottom layer + cloud-agnostic SAP layer on top.

For air-gapped SAP (defence sector, highly regulated banking core), SAP runs on private infrastructure and the air-gap discipline from the previous lesson applies: HANA media, kernel SAR files, and SPS bundles are all imported via signed bundles.


Going deeper

Everything above builds a landscape. This section is about the parts that separate a demo from a system a basis team trusts on a Saturday-night change window: idempotency on one-way installers, where check-mode helps and where it lies, the media/S-user problem, the certified path, scale, secrets, and collection versioning.

Why “idempotent” is hard for a HANA install

Ansible’s promise is idempotency: run the playbook twice, the second run changes nothing. But the underlying SAP tools — hdblcm, sapinst/SWPM — are not idempotent. Re-run hdblcm against an installed SID and it errors out (or worse, half-does something). The installer is a one-way gate, like pouring concrete.

So the idempotency lives in the wrapper, not the installer. The pattern: probe for existing state, then guard the install with when:.

# roles/sap_hana_install_wrapper/tasks/main.yml (excerpt)
- name: Detect an existing HANA installation
  ansible.builtin.stat:
    path: "/usr/sap/{{ sid }}/HDB{{ instance_number }}/exe/sapcontrol"
  register: hana_present

- name: Install HANA only when it is not already present
  ansible.builtin.include_role:
    name: redhat.sap_install.sap_hana_install
  when: not hana_present.stat.exists

The same idea makes a command:-driven service action idempotent. Never write a bare HDB start that always reports changed. Probe first, act conditionally, and set changed_when honestly:

- name: Probe HANA process state (read-only  never 'changed')
  community.sap_libs.sap_control_exec:
    sysnr: "{{ instance_number }}"
    command: GetProcessList
  register: hana_procs
  changed_when: false
  failed_when: false

- name: Start HANA only if it is not already GREEN
  community.sap_libs.sap_control_exec:
    sysnr: "{{ instance_number }}"
    command: StartSystem
  when: "'GREEN' not in (hana_procs.out | default(''))"

The two habits that make long installs safe to re-run: (1) a stat/probe task with changed_when: false in front of every irreversible step, and (2) register + changed_when/failed_when on every command/shell so Ansible’s idea of “changed” and “failed” matches reality rather than the exit code of su - <sid>adm -c ... (which is almost always 0).

Check mode: where it works and where it lies

--check (dry run) is a beginner’s favourite safety net. On SAP it is a split reality:

Layer --check behaviour What to do
OS-prep roles (sap_*_preconfigure), sysctl, mounts, packages Honest — reports the drift it would fix Run in --check as a compliance/drift probe on a schedule
Install roles (hdblcm/SWPM wrappers), HSR register, cluster build Cannot meaningfully dry-run a real installer Treat as non-check islands; rehearse on a sandbox SID

A command: task runs nothing in check mode by default, so a play that is “green in check mode” may have skipped the entire install and told you nothing. Two guard rails:

The mental model: check-mode audits your OS-prep; it does not simulate your installs. Rehearsal on a throwaway SID is the substitute for a dry-run install.

Media handling and the S-user problem

SAP media (the HANA .SAR/.EXE bundles, kernel SAR files, SPS stacks) is enormous, versioned, and only downloadable from SAP’s portal authenticated by an S-user (a support-account login like S0001234567). This is the single biggest real-world friction, and it is where secrets most often leak.

Two sane patterns, in order of preference:

  1. Pre-stage to an internal mirror (recommended). A basis engineer downloads media once to an internal artefact store / object bucket / NFS export. Hosts pull from there with an integrity check — no S-user ever touches a managed host. This is exactly the air-gapped bundle discipline.

    - name: Stage HANA server media from the internal mirror (integrity-checked, idempotent)
      ansible.builtin.get_url:
        url: "https://artifacts.internal.example/sap/HANA_2_SPS07/IMDB_SERVER.SAR"
        dest: "/sapmedia/HANA_2_SPS07/IMDB_SERVER.SAR"
        checksum: "sha256:{{ hana_server_sar_sha256 }}"
        mode: "0640"
    
    - name: Extract the SAR with SAPCAR
      community.sap_libs.sapcar_extract:
        path: "/sapmedia/HANA_2_SPS07/IMDB_SERVER.SAR"
        dest: "/sapmedia/HANA_2_SPS07/extracted"
        binary_path: "/sapmedia/tools/SAPCAR"
    

    get_url with checksum: is both idempotent (skips an already-correct file) and tamper-evident — the two properties that matter for “the install failed because the media was truncated,” which is the most common HANA install bug.

  2. Download by number with the community.sap_launchpad collection. For estates that must pull directly, the S-user credentials live only in Vault, are passed with no_log: true, and are never templated into a host file.

    # group_vars/all/vault.yml  → ansible-vault encrypt this file. Values are PLACEHOLDERS.
    vault_sap_suser:          "S0000000000"        # your S-user id
    vault_sap_suser_password: "REPLACE_VIA_VAULT"  # never a real secret in git
    

Whichever pattern you pick: checksum every artefact, extract to a deterministic path, and keep the S-user out of managed hosts and out of git.

Post-install config the idempotent way (Notes and task lists)

Once a system is running, day-2 configuration should stop shelling out and start using purpose-built, idempotent modules from community.sap_libs:

- name: Apply a corrective SAP Note (idempotent  skips if already implemented)
  community.sap_libs.sap_snote:
    conn_username: "DDIC"
    conn_password: "{{ vault_ddic_password }}"
    host: "{{ ascs_vip }}"
    sysnr: "00"
    client: "000"
    snote_path: "/sapmedia/notes/0003089413.txt"
  no_log: true

- name: Run a post-install ABAP task list (STC01  idempotent by design)
  community.sap_libs.sap_task_list_execute:
    conn_username: "DDIC"
    conn_password: "{{ vault_ddic_password }}"
    host: "{{ ascs_vip }}"
    sysnr: "00"
    client: "000"
    task_to_execute: "SAP_BASIS_SSL_CHECK"
  no_log: true

sap_snote checks whether the Note is already implemented and no-ops if so — that is real idempotency for stateful application config, not a command: wrapper pretending. Task lists (transaction STC01) are the ABAP world’s own idempotent runbooks; driving them from Ansible means “post-install step 14” is versioned code, not a wiki page.

The certified reference architectures — and why you stay on them

SAP support is conditional. Run HANA on an un-certified stack and, when you open a priority-one ticket at hour three of an outage, the first response may be “not a supported configuration.” So the automation’s job is not just “install HANA” — it is “install HANA on the certified path and prove it.” Concretely that means:

The Ansible pay-off: the certified choices become variables with locked defaults in a wrapper role, so “are we still on the supported path?” is answered by reading group_vars, and drift away from it is a failed assertion, not a surprise during a Sev-1.

Performance and long-running tasks at landscape scale

The control node is almost never the bottleneck in SAP automation — the SAP hosts are (an hdblcm run is tens of minutes; an SPS upgrade can be hours). Two levers matter:

forks and fact caching help a 200-host landscape’s fast phases (OS-prep, probes); they do nothing for the slow single-host installer steps — those are gated by the SAP tools, so plan windows around them.

Secrets: the SAP vault sprawl

SAP has an unusually large secret surface. A single production HANA + NetWeaver stack can involve: the HANA master password, the <sid>adm OS password, the DDIC/SYSTEM DB users, SYSTEM schema owner, the SAP* superuser, the S-user, pcsd/hacluster cluster passwords, the STONITH fence credential, and the backint backup credential. Every one of them is a plaintext-leak waiting to happen.

Discipline (built on the Vault lesson):

Pinning collections and tracking SAP releases

The SAP collections move fast because SAP does. Two consequences:


Anti-patterns that destroy SAP automation


Common beginner mistakes

These are the misconceptions that trip up engineers new to SAP-on-Ansible — distinct from the operational anti-patterns above. Each is a wrong mental model and the right one to replace it with.


Practice challenges

Work these in order — they escalate from “get the collections on your box” to “gate a change on a live round-trip.” No licensed SAP media is required to write and lint them; where a step needs a running SAP system, treat it as a rehearsal against a sandbox SID. Each solution notes the one idea it is teaching.

1. (Beginner) Pin and install the SAP collections. Write a collections/requirements.yml that pins the collections this lesson uses, then install them.

<details> <summary>Solution</summary>

# collections/requirements.yml
collections:
  - name: redhat.sap_install
    version: "1.4.1"
  - name: community.sap_libs
    version: ">=1.4.2"
  - name: ansible.posix
  - name: community.general
ansible-galaxy collection install -r collections/requirements.yml
ansible-galaxy collection list | grep -Ei 'sap|posix'

Why: pinning makes runs reproducible; an unpinned collection can change installer behaviour under you across an SPS boundary. </details>

2. (Beginner) Fail fast with a preflight assertion. Before any install task, abort if the host has less than 4 GB RAM or is not subscribed to a SAP-solutions repo.

<details> <summary>Solution</summary>

- name: Read enabled repositories (read-only  must never report 'changed')
  ansible.builtin.command: subscription-manager repos --list-enabled
  register: repos
  changed_when: false

- name: Preflight  refuse to install on an unfit host
  ansible.builtin.assert:
    that:
      - ansible_facts.memtotal_mb >= 4096
      - "'sap-solutions' in repos.stdout"
    fail_msg: "Host is not SAP-ready (RAM below 4 GB or missing SAP-solutions repo)."
    success_msg: "Preflight passed."

Why: assert evaluates even in --check, and changed_when: false keeps the read-only probe honest — you catch an unfit host before the one-way installer touches it. </details>

3. (Intermediate) Make a HANA start task genuinely idempotent. Replace a bare HDB start with a probe-then-act pattern so a second run reports no change.

<details> <summary>Solution</summary>

- name: Probe process state (read-only)
  community.sap_libs.sap_control_exec:
    sysnr: "{{ instance_number }}"
    command: GetProcessList
  register: procs
  changed_when: false
  failed_when: false

- name: Start the system only if it is not already GREEN
  community.sap_libs.sap_control_exec:
    sysnr: "{{ instance_number }}"
    command: StartSystem
  when: "'GREEN' not in (procs.out | default(''))"

Why: the installer/service tools are not idempotent — your wrapper is. Probe with changed_when: false, then guard the mutation with when:. </details>

4. (Intermediate) Stage media with an integrity gate. Pull a HANA SAR from an internal mirror, fail on checksum mismatch, then extract it with SAPCAR.

<details> <summary>Solution</summary>

- name: Fetch media (idempotent + tamper-evident)
  ansible.builtin.get_url:
    url: "https://artifacts.internal.example/sap/HANA_2_SPS07/IMDB_SERVER.SAR"
    dest: "/sapmedia/HANA_2_SPS07/IMDB_SERVER.SAR"
    checksum: "sha256:{{ hana_server_sar_sha256 }}"
    mode: "0640"

- name: Extract with SAPCAR
  community.sap_libs.sapcar_extract:
    path: "/sapmedia/HANA_2_SPS07/IMDB_SERVER.SAR"
    dest: "/sapmedia/HANA_2_SPS07/extracted"
    binary_path: "/sapmedia/tools/SAPCAR"

Why: truncated/tampered media is the most common HANA install failure; get_url’s checksum: makes the download both idempotent and verified before the installer ever sees it. </details>

5. (Advanced) Roll a SAP-kernel patch in the correct order. Patch one instance at a time, ERS before ASCS, dialogs one by one — using ordering, not luck.

<details> <summary>Solution</summary>

- name: Rolling SAP kernel patch (ERS  ASCS  dialogs, one at a time)
  hosts: sap_landscape        # inventory ordered: ers, ascs, pas, aas
  become: true
  serial: 1
  order: inventory            # honour that inventory order, host by host
  collections:
    - community.sap_libs
  tasks:
    - name: Stop this instance gracefully
      community.sap_libs.sap_control_exec:
        sysnr: "{{ instance_number }}"
        command: Stop

    - name: Apply the kernel SAR files (backup + symlink + saproot.sh)
      ansible.builtin.import_role:
        name: sap_kernel_patch

    - name: Start this instance again
      community.sap_libs.sap_control_exec:
        sysnr: "{{ instance_number }}"
        command: Start

Why: serial: 1 + order: inventory turns a landscape-wide outage into a controlled wave — ERS drains locks before ASCS is touched, and each dialog leaves/rejoins the load balancer cleanly. </details>

6. (Advanced) Gate the change on a live round-trip. After the operation, fail the play unless a synthetic RFC echoes back — proving VIP → dispatcher → work process is alive.

<details> <summary>Solution</summary>

- name: Post-op smoke test  RFC round-trip
  community.sap_libs.sap_pyrfc:
    function: "STFC_CONNECTION"
    parameters:
      REQUTEXT: "ansible-{{ ansible_date_time.epoch }}"
    connection:
      ashost: "{{ ascs_vip }}"
      sysnr: "00"
      client: "100"
      user: "{{ vault_smoke_user }}"
      passwd: "{{ vault_smoke_password }}"
      lang: "EN"
  register: rfc
  no_log: true

- name: Fail the play if the echo does not round-trip
  ansible.builtin.assert:
    that:
      - "(rfc.result.ECHOTEXT | default('')) == ('ansible-' ~ ansible_date_time.epoch)"
    fail_msg: "SAP stack not reachable after the operation — investigate before declaring success."

(The exact return key — rfc.result.* vs rfc.return_value.* — depends on the module version; check --doc. Representative.)

Why: a service that is “started” is not proven “working.” A synthetic STFC_CONNECTION exercises the whole path end-to-end, so a broken dispatcher fails the change instead of shipping it. </details>


Frequently asked questions

1. Can I install HANA without using redhat.sap_hana_install? Technically yes, but you will reinvent the answer-file generation, license application, and post-install parameter setting. The collection encodes thousands of lines of basis knowledge; not using it is a guarantee of bugs.

2. What’s the right HANA replication mode for my landscape? Same DC: Sync (RPO=0, performance hit) or SyncMem (RPO≈0). Cross-region: Async (some loss possible on forced takeover). Hybrid (HSR as DR): Async with a 5-min lag alarm and a tested forced-takeover playbook.

3. How do I patch the Linux kernel under HANA? Run redhat.sap_management.sap_hana_set_takeover to controlled-failover to secondary. Patch primary’s Linux kernel. Reboot. Re-establish HSR. Failback. This is one of the rarer-used routines and benefits the most from rehearsal.

4. Can Ansible handle SAP transport routes (TMS configuration)? Yes, via tp CLI calls in playbooks. The TMS GUI is one-time configuration; routine transport imports are the recurring work, and they automate cleanly.

5. What about SAP S/4HANA Cloud (private edition)? Same patterns. The customer still owns the infrastructure layer; Ansible automates HANA install, NetWeaver install, kernel patches, transports. SAP only operates the “managed services” wrapper above your stack.

6. How do I integrate Ansible with Solution Manager (SolMan)? SolMan can import job execution history via RFC; you pump Ansible job results back via sap_rfc_call. Most basis teams treat AAP as the orchestrator and SolMan as the change repository; SolMan owns the change/release record, AAP owns the execution.

7. What’s the right failure mode for a transport import that gets RC=8? Stop the workflow, page the basis lead. RC=8 means object-related issues that need human inspection. Do NOT auto-retry; you may mask a real syntax error.

8. How big should HANA backups be in production? Daily full + every-15min log backup is the SAP-recommended baseline. Use HANA’s native backup with a backint-compatible target (Veeam, Commvault, or HANA on AWS Backup with the AWS backint adapter). Ansible owns the backup-job creation and rotation policy; the actual data movement is the backint plug-in’s job.

9. Can I use Ansible to drive SAP MaxDB or ASE? Yes, with community.sap_install having modules for both. They are less commonly needed than HANA but follow the same install/patch/configure pattern.

10. What’s the single most underrated SAP automation practice? The per-SAP-Note role. When SAP issues a new Note that affects your platform (e.g., a kernel-tuning Note for memory leaks), encode it as a small Ansible role with a clear when guard, run it on the test landscape, and add it to the OS-prep workflow. Six months later, when an auditor asks “is Note 2382421 applied?”, the answer is “yes, and here is the ledger entry showing every host has run that role.”


Hands-on lab — first SAP-aware Ansible play

A full HANA install needs SAP licensed media. The following lab uses publicly available SAP-related tooling to get hands-on without a license: setting up the OS prerequisites a HANA install would expect.

Prerequisites: RHEL 8/9 VM with at least 4GB RAM, ansible-core ≥ 2.16.

mkdir -p sap-lab/{playbooks,roles}
cd sap-lab
ansible-galaxy collection install redhat.sap_install
# playbooks/os-prep.yml
- hosts: localhost
  become: true
  collections:
    - redhat.sap_install
  roles:
    - role: sap_general_preconfigure
      vars:
        sap_general_preconfigure_modify_etc_hosts: false
    - role: sap_hana_preconfigure
      vars:
        sap_hana_preconfigure_thp: never
ansible-playbook playbooks/os-prep.yml -K
sysctl kernel.shmmax     # huge value
cat /sys/kernel/mm/transparent_hugepage/enabled   # [never]
tuned-adm active         # sap-hana
ulimit -n -H -S

Now read what the role did:

cat /etc/security/limits.d/99-sap.conf
cat /etc/sysctl.d/sap.conf
ansible-galaxy role list redhat.sap_install

You have just executed a non-trivial fraction of the OS work that goes into every HANA install, and seen the artefacts the SAP basis role leaves behind. Extend the lab by writing a roles/sap_storage_layout that creates the /hana/{data,log,shared} mountpoints (without real disks, use tmpfs); rerun and confirm idempotency.

Then take it one step further (ties the lesson together): run the same play again with --check --diff. Watch the OS-prep roles report no changes (they are idempotent) — that is the drift-probe from Going deeper in action. Now add the Challenge-2 preflight assert at the top and re-run in check mode; confirm the assertion still evaluates even though nothing is applied. That contrast — asserts run in check mode, installers do not — is the single most useful thing to internalise before you touch a real SID.


Glossary


Certification mapping


Next steps

You now have an opinionated, Ansible-driven view of the SAP basis function. The remaining specialist lessons cover:

If you only take one habit from this lesson: always go through the SAP-supported collections. They are not perfect, but they encode hundreds of person-years of basis knowledge, and the alternative is a fork of hdblcm invocations that you will maintain forever.

ansiblesaphananetweaversap-basishana-replicationsap-on-rhelkernel-patching
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments