---
# This task file ensures that a Cloudflare "Dynamic Redirect Rule" exists
# which redirects www.example.com โ https://example.com{uri} with 301.
# Required vars to pass in:
# www_fqdn: "www.example.com"
# apex_url: "https://example.com"
# API.cloudflare.api_token (via lookup('api', 'cloudflare.api_token'))
- name: "๐งฎ Derive zone from www_fqdn; apex comes from caller"
set_fact:
cf_zone_name: "{{ www_fqdn | to_zone }}"
domain_apex: "{{ domain }}"
- name: "๐ Cloudflare: Lookup zone id for {{ cf_zone_name }}"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
uri_retry:
url: "https://api.cloudflare.com/client/v4/zones?name={{ cf_zone_name }}"
method: GET
headers:
Authorization: "Bearer {{ lookup('api', 'cloudflare.api_token') }}"
Content-Type: "application/json"
return_content: true
register: cf_zone_lookup
failed_when: >
(cf_zone_lookup.status != 200) or
(cf_zone_lookup.json.result | length) == 0
- name: "๐ง Set fact: zone_id"
set_fact:
cf_zone_id: "{{ (cf_zone_lookup.json.result | first).id }}"
- name: "๐๏ธ Remove conflicting A/AAAA record for {{ www_fqdn }}"
community.general.cloudflare_dns:
api_token: "{{ lookup('api', 'cloudflare.api_token') }}"
zone: "{{ cf_zone_name }}"
type: "A"
name: "{{ www_fqdn }}"
state: absent
ignore_errors: true
- name: "๐ Cloudflare DNS: ensure {{ www_fqdn }} is proxied (CNAME)"
community.general.cloudflare_dns:
api_token: "{{ lookup('api', 'cloudflare.api_token') }}"
zone: "{{ cf_zone_name }}"
type: "CNAME"
name: "{{ www_fqdn }}"
value: "{{ domain_apex }}"
proxied: true
ttl: 1
state: present
- name: "๐ Cloudflare Rulesets: list dynamic redirect rulesets"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
uri_retry:
url: "https://api.cloudflare.com/client/v4/zones/{{ cf_zone_id }}/rulesets?phase=http_request_dynamic_redirect"
method: GET
headers:
Authorization: "Bearer {{ lookup('api', 'cloudflare.api_token') }}"
Content-Type: "application/json"
return_content: true
register: cf_rulesets
- name: "๐ฏ Pick existing custom ruleset (if any)"
set_fact:
cf_redirect_ruleset: >-
{{
(cf_rulesets.json.result | default([]))
| selectattr('kind','equalto','zone')
| list
| first
}}
cf_redirect_ruleset_id: "{{ (cf_redirect_ruleset.id | default('')) }}"
- name: "๐จ Build desired redirect rule object"
set_fact:
desired_redirect_rule:
ref: "redirect_www_to_apex"
description: "Redirect www โ apex (301)"
expression: "http.host eq \"{{ www_fqdn }}\""
action: "redirect"
action_parameters:
from_value:
target_url:
expression: "\"{{ apex_url }}\" + http.request.uri.path + (len(http.request.uri.query) > 0 ? (\"?\" + http.request.uri.query) : \"\")"
status_code: 301
preserve_query_string: true
- name: "๐ Update existing dynamic redirect ruleset (PUT)"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
when: cf_redirect_ruleset_id | length > 0
vars:
existing_rules: "{{ cf_redirect_ruleset.rules | default([]) }}"
merged_rules: >-
{{
(
(existing_rules | rejectattr('ref','equalto', desired_redirect_rule.ref) | list)
+ [ desired_redirect_rule ]
)
}}
uri_retry:
url: "https://api.cloudflare.com/client/v4/zones/{{ cf_zone_id }}/rulesets/{{ cf_redirect_ruleset_id }}"
method: PUT
headers:
Authorization: "Bearer {{ lookup('api', 'cloudflare.api_token') }}"
Content-Type: "application/json"
status_code: 200
body_format: json
body:
name: "{{ cf_redirect_ruleset.name | default('www-to-apex') }}"
kind: "zone"
phase: "http_request_dynamic_redirect"
rules: "{{ merged_rules }}"
register: cf_ruleset_update
changed_when: cf_ruleset_update.status in [200]
- name: "๐ Create dynamic redirect ruleset (POST)"
no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
when: cf_redirect_ruleset_id | length == 0
uri_retry:
url: "https://api.cloudflare.com/client/v4/zones/{{ cf_zone_id }}/rulesets"
method: POST
headers:
Authorization: "Bearer {{ lookup('api', 'cloudflare.api_token') }}"
Content-Type: "application/json"
status_code: 200
body_format: json
body:
name: "www-to-apex"
kind: "zone"
phase: "http_request_dynamic_redirect"
rules:
- "{{ desired_redirect_rule }}"
register: cf_ruleset_create
changed_when: cf_ruleset_create.status in [200]