Files
test/roles/ensure_apache/tasks/main.yml
T
jmrothst eb2dd37385 feat(ensure_apache): synchronize website content from local temp staging
Replace remote git checkouts on target web nodes with an orchestrated
local staging and rsync deployment pipeline.

Recent OpenSSH updates in Fedora dropped agent forwarding into remote
sessions, breaking remote git fetch operations. Furthermore, pulling
directly from Git on production servers exposed .git repositories in
public webroots and required target servers to hold Git credentials.

- Create an ephemeral, unguessable local staging directory (0700) on
  localhost via ansible.builtin.tempfile scoped by inventory_hostname.
- Stage checkouts using repo hash prefixes to prevent collision when
  public and private servers share identical FQDNs.
- Execute shallow checkouts (depth: 1) on the control node using local
  credentials.
- Push website content to /srv/http/{fqdn}/ via ansible.posix.synchronize
  with checksum delta comparisons and automated cleanup (--delete).
- Introduce ensure_apache_rsync_excludes in role defaults to strip
  .git*, .env*, CI/CD pipelines (.gitea, .github), editor swap files,
  and OS metadata while preserving .htaccess and /.well-known/.
- Enforce httpd_sys_content_t SELinux context recursively across webroots.
- Guarantee staging directory cleanup on localhost via an always block.
2026-09-07 23:35:05 -05:00

283 lines
9.4 KiB
YAML

---
# tasks file for ensure_apache
- name: Assert Target OS Is Supported
when:
- ansible_facts["system"] is defined
ansible.builtin.assert:
fail_msg: "Unsupported OS: {{ ansible_facts['distribution'] | default('unknown') }}"
quiet: true
that:
- ansible_facts["system"] == 'Linux'
- >-
ansible_facts["distribution"] in ['AlmaLinux', 'CentOS', 'Debian',
'Fedora', 'OracleLinux', 'Rocky', 'Ubuntu']
- name: Include Variables
ansible.builtin.include_vars:
file: '{{ lookup("first_found", findme) }}'
name: ensure_apache
vars:
findme:
files:
- '{{ ansible_facts["distribution"] }}-{{ ansible_facts["distribution_major_version"]
}}-{{ ansible_facts["architecture"] }}.yml'
- '{{ ansible_facts["distribution"] }}-{{ ansible_facts["distribution_major_version"]
}}-default.yml'
- '{{ ansible_facts["distribution"] }}-default.yml'
- '{{ ansible_facts["os_family"] }}-{{ ansible_facts["distribution_major_version"]
}}-{{ ansible_facts["architecture"] }}.yml'
- '{{ ansible_facts["os_family"] }}-{{ ansible_facts["distribution_major_version"]
}}-default.yml'
- '{{ ansible_facts["os_family"] }}-default.yml'
- default.yml
paths: [../vars/]
errors: ignore
- name: Package Discovery
when:
- ansible_facts["packages"] is not defined
ansible.builtin.package_facts:
- name: Service Discovery
when:
- ansible_facts["services"] is not defined
ansible.builtin.service_facts:
- name: Ensure Sysctl
when:
- ensure_apache is defined
- ensure_apache.sysctl_list is defined
- ensure_apache.sysctl_list is iterable
ansible.posix.sysctl:
name: '{{ item.name }}'
reload: '{{ item.reload | default(omit) }}'
state: '{{ item.state }}'
sysctl_file: '{{ item.sysctl_file | default(omit) }}'
sysctl_set: '{{ item.sysctl_set | default(omit) }}'
value: '{{ item.value | default(omit) }}'
loop: '{{ ensure_apache.sysctl_list }}'
loop_control:
label: '{{ item.name }} will be {{ item.value }}'
notify:
- Ensure_Apache.Package_Facts
- Ensure_Apache.Service_Facts
- Ensure_Apache.Service_Reload
- Ensure_Apache.Service_Reload_Config
- name: Ensure Packages
when:
- ensure_apache is defined
- ensure_apache.package_list is defined
- ensure_apache.package_list is iterable
ansible.builtin.package:
name: '{{ item.name }}'
state: '{{ item.state }}'
loop: '{{ ensure_apache.package_list }}'
loop_control:
label: '{{ item.name }} will be {{ item.state }}'
notify:
- Ensure_Apache.Package_Facts
- Ensure_Apache.Service_Facts
- Ensure_Apache.Service_Reload
- Ensure_Apache.Service_Reload_Config
- name: Ensure Seboolean
when:
- ensure_apache is defined
- ensure_apache.seboolean_list is defined
- ensure_apache.seboolean_list is iterable
ansible.posix.seboolean:
name: '{{ item.name }}'
persistent: '{{ item.persistent }}'
state: '{{ item.state }}'
loop: '{{ ensure_apache.seboolean_list }}'
loop_control:
label: '{{ item.name }} will be {{ item.state }}'
notify:
- Ensure_Apache.Package_Facts
- Ensure_Apache.Service_Facts
- Ensure_Apache.Service_Reload
- Ensure_Apache.Service_Reload_Config
- name: Ensure Configurations
when:
- http_vhost is defined
- ensure_apache is defined
- ensure_apache.template_list is defined
- ensure_apache.template_list is iterable
ansible.builtin.template:
backup: 'no'
dest: '{{ item.dest }}'
group: '{{ item.group | default(omit) }}'
mode: '{{ item.mode | default(omit) }}'
owner: '{{ item.owner | default(omit) }}'
selevel: '{{ item.selevel | default(omit) }}'
serole: '{{ item.serole | default(omit) }}'
setype: '{{ item.setype | default(omit) }}'
seuser: '{{ item.seuser | default(omit) }}'
src: '{{ item.src }}'
loop: '{{ ensure_apache.template_list }}'
loop_control:
label: '{{ item.dest }} will be ensured'
notify:
- Ensure_Apache.Package_Facts
- Ensure_Apache.Service_Facts
- Ensure_Apache.Service_Reload
- Ensure_Apache.Service_Reload_Config
- name: Ensure Firewall
when:
- ansible_facts.packages["firewalld"] is defined
- ansible_facts.packages["python3-firewall"] is defined
- ensure_apache is defined
- ensure_apache.firewalld_list is defined
- ensure_apache.firewalld_list is iterable
ansible.posix.firewalld:
permanent: '{{ item.permanent }}'
service: '{{ item.service }}'
state: '{{ item.state }}'
loop: '{{ ensure_apache.firewalld_list }}'
loop_control:
label: '{{ item.service }} will be {{ item.state }}'
notify:
- Ensure_Apache.Package_Facts
- Ensure_Apache.Service_Facts
- Ensure_Apache.Service_Reload
- Ensure_Apache.Service_Reload_Config
- name: Ensure Permissions
when:
- ensure_apache is defined
- http_vhost is defined
- http_vhost is iterable
- ensure_apache.permission_list is defined
- ensure_apache.permission_list is iterable
ansible.builtin.file:
attributes: '{{ item.attributes | default(omit) }}'
follow: '{{ item.follow | default(omit) }}'
force: '{{ item.force | default(omit) }}'
group: '{{ item.group | default(omit) }}'
owner: '{{ item.owner | default(omit) }}'
mode: '{{ item.mode | default(omit) }}'
path: '{{ item.path }}'
reuse: '{{ item.reuse | default(omit) }}'
selevel: '{{ item.selevel | default(omit) }}'
serole: '{{ item.serole | default(omit) }}'
setype: '{{ item.setype | default(omit) }}'
seuser: '{{ item.seuser | default(omit) }}'
src: '{{ item.src | default(omit) }}'
state: '{{ item.state }}'
loop: '{{ ensure_apache.permission_list }}'
loop_control:
label: '{{ item.path }} will be ensured'
notify:
- Ensure_Apache.Package_Facts
- Ensure_Apache.Service_Facts
- Ensure_Apache.Service_Reload
- Ensure_Apache.Service_Reload_Config
- name: Ensure Vhost Document Roots
when:
- ensure_apache is defined
- http_vhost is defined
- http_vhost is iterable
- item.fqdn is defined
ansible.builtin.file:
path: /srv/http/{{ item.fqdn }}
state: directory
owner: root
group: root
mode: '0755'
setype: httpd_sys_content_t
loop: '{{ http_vhost }}'
loop_control:
label: /srv/http/{{ item.fqdn }} will be ensured
notify:
- Ensure_Apache.Package_Facts
- Ensure_Apache.Service_Facts
- Ensure_Apache.Service_Reload
- Ensure_Apache.Service_Reload_Config
- name: Synchronize Website Content From Git Repositories
when:
- ensure_apache is defined
- http_vhost is defined
- http_vhost is iterable
- http_vhost | selectattr('repo', 'defined') | list | length > 0
block:
- name: Create Temporary Local Staging Directory
become: false
delegate_to: localhost
ansible.builtin.tempfile:
prefix: 'ansible_web_{{ inventory_hostname }}_'
state: directory
register: ensure_apache_local_tempdir
- name: Ensure Website Content Cloned Locally # noqa: latest[git]
when:
- item.fqdn is defined
- item.repo is defined
become: false
delegate_to: localhost
ansible.builtin.git:
accept_hostkey: true
depth: 1
dest: '{{ ensure_apache_local_tempdir.path }}/{{ (item.repo | hash("sha256"))[:8] }}_{{ item.fqdn }}'
repo: '{{ item.repo }}'
version: '{{ item.version | default("HEAD") }}'
loop: '{{ http_vhost }}'
loop_control:
label: '{{ item.fqdn }} local clone'
- name: Ensure Website Content Synchronized To Target
when:
- item.fqdn is defined
- item.repo is defined
ansible.posix.synchronize:
archive: true
checksum: true
delete: true
dest: '/srv/http/{{ item.fqdn }}/'
rsync_opts: '{{ item.rsync_opts | default(ensure_apache_rsync_excludes) }}'
src: '{{ ensure_apache_local_tempdir.path }}/{{ (item.repo | hash("sha256"))[:8] }}_{{ item.fqdn }}/'
loop: '{{ http_vhost }}'
loop_control:
label: '/srv/http/{{ item.fqdn }} will be synchronized'
notify:
- Ensure_Apache.Service_Reload
- Ensure_Apache.Service_Reload_Config
- name: Ensure SELinux Context On Web Content
when:
- item.fqdn is defined
- item.repo is defined
ansible.builtin.file:
dest: '/srv/http/{{ item.fqdn }}'
group: root
mode: '0755'
owner: root
recurse: true
setype: httpd_sys_content_t
loop: '{{ http_vhost }}'
loop_control:
label: '/srv/http/{{ item.fqdn }} SELinux context'
always:
- name: Remove Temporary Local Staging Directory
when:
- ensure_apache_local_tempdir is defined
- ensure_apache_local_tempdir.path is defined
become: false
delegate_to: localhost
ansible.builtin.file:
path: '{{ ensure_apache_local_tempdir.path }}'
state: absent
- name: Ensure Services
when:
- ensure_apache is defined
- ensure_apache.service_list is defined
- ensure_apache.service_list is iterable
ansible.builtin.service:
enabled: '{{ item.enabled }}'
name: '{{ item.name }}'
state: '{{ item.state }}'
loop: '{{ ensure_apache.service_list }}'
loop_control:
label: '{{ item.name }} will be {{ item.state }}'
notify:
- Ensure_Apache.Package_Facts
- Ensure_Apache.Service_Facts
- Ensure_Apache.Service_Reload
- Ensure_Apache.Service_Reload_Config
- name: Flush Handlers
ansible.builtin.meta: flush_handlers