mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2025-08-29 23:08:06 +02:00
refactor(dns): unify Cloudflare + Hetzner handling across roles
- replaced CERTBOT_DNS_API_TOKEN with CLOUDFLARE_API_TOKEN everywhere - introduced generic sys-dns-cloudflare-records role for managing DNS records - added sys-dns-hetzner-rdns role with both Cloud (hcloud) and Robot API flavors - updated Mailu role to: - generate DKIM before DNS setup - delegate DNS + rDNS records to the new generic roles - removed legacy per-role Cloudflare vars (MAILU_CLOUDFLARE_API_TOKEN) - extended group vars with HOSTING_PROVIDER for rDNS flavor decision - added hetzner.hcloud collection to requirements This consolidates DNS management into reusable roles, supports both Cloudflare and Hetzner providers, and standardizes variable naming across the project.
This commit is contained in:
24
roles/sys-dns-cloudflare-records/README.md
Normal file
24
roles/sys-dns-cloudflare-records/README.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# 🌐 Cloudflare DNS Records
|
||||
|
||||
## Description
|
||||
|
||||
Generic, data-driven role to manage DNS records on Cloudflare (A/AAAA, CNAME, MX, TXT, SRV).
|
||||
Designed for reuse across apps (e.g., Mailu) and environments.
|
||||
|
||||
## Overview
|
||||
|
||||
This role wraps `community.general.cloudflare_dns` and applies records from a single
|
||||
structured variable (`cloudflare_records`). It supports async operations and
|
||||
can be used to provision all required records for a service in one task.
|
||||
|
||||
## Features
|
||||
|
||||
- Data-driven input for multiple record types
|
||||
- Supports A/AAAA, CNAME, MX, TXT, SRV
|
||||
- Optional async execution
|
||||
- Minimal logging of secrets
|
||||
|
||||
## Further Resources
|
||||
|
||||
- [Cloudflare Dashboard → API Tokens](https://dash.cloudflare.com/profile/api-tokens)
|
||||
- [Ansible Collection: community.general.cloudflare_dns](https://docs.ansible.com/ansible/latest/collections/community/general/cloudflare_dns_module.html)
|
12
roles/sys-dns-cloudflare-records/defaults/main.yml
Normal file
12
roles/sys-dns-cloudflare-records/defaults/main.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
# Cloudflare API Token
|
||||
# More information here: group_vars/all/docs/CLOUDFLARE_API_TOKEN.md
|
||||
CLOUDFLARE_API_TOKEN: ""
|
||||
|
||||
cloudflare_async_enabled: "{{ ASYNC_ENABLED | bool }}"
|
||||
cloudflare_async_time: "{{ ASYNC_TIME }}"
|
||||
cloudflare_async_poll: "{{ ASYNC_POLL }}"
|
||||
cloudflare_no_log: "{{ MASK_CREDENTIALS_IN_LOGS | bool }}"
|
||||
|
||||
# Supported types:
|
||||
# A/AAAA (content), CNAME/MX/TXT (value, MX hat priority), SRV (service/proto/…)
|
||||
cloudflare_records: []
|
@@ -0,0 +1,61 @@
|
||||
# Cloudflare API Token for Ansible (`CLOUDFLARE_API_TOKEN`)
|
||||
|
||||
This document explains how to generate and use a Cloudflare API Token for DNS automation and certificate operations in Ansible (e.g., with Certbot).
|
||||
|
||||
## Purpose
|
||||
|
||||
The `CLOUDFLARE_API_TOKEN` variable must contain a valid Cloudflare API Token.
|
||||
This token is used for all DNS operations and ACME (SSL/TLS certificate) challenges that require access to your Cloudflare-managed domains.
|
||||
|
||||
**Never commit your API token to a public repository. Always keep it secure!**
|
||||
|
||||
---
|
||||
|
||||
## How to Create a Cloudflare API Token
|
||||
|
||||
### 1. Log In to Cloudflare
|
||||
|
||||
- Go to: [https://dash.cloudflare.com/](https://dash.cloudflare.com/) and log in.
|
||||
|
||||
### 2. Open the API Tokens Page
|
||||
|
||||
- Click your profile icon (top right) → **My Profile**
|
||||
- In the sidebar, choose **API Tokens**
|
||||
Or use this direct link: [https://dash.cloudflare.com/profile/api-tokens](https://dash.cloudflare.com/profile/api-tokens)
|
||||
|
||||
### 3. Click **Create Token**
|
||||
|
||||
### 4. Select **Custom Token**
|
||||
|
||||
- Give your token a descriptive name (e.g., `Ansible Certbot Automation`).
|
||||
|
||||
### 5. Set Permissions
|
||||
|
||||
Add the following permissions:
|
||||
|
||||
| Category | Permission | Access |
|
||||
| -------- | ------------ | -------- |
|
||||
| Zone | Zone | Read |
|
||||
| Zone | DNS | Edit |
|
||||
| Zone | Cache Purge | Purge |
|
||||
|
||||
- These permissions are required for DNS record management, CAA/SPF/DKIM handling, cache purging, and certificate provisioning.
|
||||
|
||||
### 6. Zone Resources
|
||||
|
||||
- **Zone Resources:** Set to `Include → All zones`
|
||||
(Or restrict to specific zones as needed for your environment.)
|
||||
|
||||
### 7. Create and Save the Token
|
||||
|
||||
- Click **Continue to summary** and then **Create Token**.
|
||||
- Copy the API Token. **It will only be shown once!**
|
||||
|
||||
---
|
||||
|
||||
## Using the Token in Ansible
|
||||
|
||||
Set the token in your Ansible inventory or secrets file:
|
||||
|
||||
```yaml
|
||||
CLOUDFLARE_API_TOKEN: "cf_your_generated_token_here"
|
23
roles/sys-dns-cloudflare-records/meta/main.yml
Normal file
23
roles/sys-dns-cloudflare-records/meta/main.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
---
|
||||
galaxy_info:
|
||||
author: "Kevin Veen-Birkenbach"
|
||||
description: "Generic role to manage Cloudflare DNS records (A/AAAA, CNAME, MX, TXT, SRV) in a data-driven way."
|
||||
license: "Infinito.Nexus NonCommercial License"
|
||||
license_url: "https://s.infinito.nexus/license"
|
||||
company: |
|
||||
Kevin Veen-Birkenbach
|
||||
Consulting & Coaching Solutions
|
||||
https://www.veen.world
|
||||
galaxy_tags:
|
||||
- cloudflare
|
||||
- dns
|
||||
- records
|
||||
- mail
|
||||
- automation
|
||||
repository: "https://s.infinito.nexus/code"
|
||||
issue_tracker_url: "https://s.infinito.nexus/issues"
|
||||
documentation: "https://docs.infinito.nexus"
|
||||
logo:
|
||||
class: "fa-solid fa-cloud"
|
||||
run_after: []
|
||||
dependencies: []
|
105
roles/sys-dns-cloudflare-records/tasks/main.yml
Normal file
105
roles/sys-dns-cloudflare-records/tasks/main.yml
Normal file
@@ -0,0 +1,105 @@
|
||||
---
|
||||
# run_once_sys_dns_cloudflare_records: deactivated
|
||||
|
||||
- name: Assert token
|
||||
ansible.builtin.assert:
|
||||
that: [ "CLOUDFLARE_API_TOKEN | length > 0" ]
|
||||
no_log: "{{ cloudflare_no_log | bool }}"
|
||||
|
||||
- name: Apply A/AAAA
|
||||
community.general.cloudflare_dns:
|
||||
api_token: "{{ CLOUDFLARE_API_TOKEN }}"
|
||||
zone: "{{ item.zone }}"
|
||||
type: "{{ item.type }}"
|
||||
name: "{{ item.name }}"
|
||||
content: "{{ item.content }}"
|
||||
proxied: "{{ item.proxied | default(false) }}"
|
||||
ttl: "{{ item.ttl | default(1) }}"
|
||||
state: "{{ item.state | default('present') }}"
|
||||
loop: "{{ cloudflare_records | selectattr('type','in',['A','AAAA']) | list }}"
|
||||
loop_control: { label: "{{ item.type }} {{ item.name }} -> {{ item.content }}" }
|
||||
async: "{{ cloudflare_async_enabled | ternary(cloudflare_async_time, omit) }}"
|
||||
poll: "{{ cloudflare_async_enabled | ternary(cloudflare_async_poll, omit) }}"
|
||||
no_log: "{{ cloudflare_no_log | bool }}"
|
||||
register: _cf_call
|
||||
failed_when: >
|
||||
_cf_call is failed and
|
||||
(
|
||||
('An identical record already exists' not in (_cf_call.msg | default('') | string))
|
||||
and
|
||||
('81058' not in (_cf_call.msg | default('') | string))
|
||||
)
|
||||
changed_when: >
|
||||
(_cf_call.changed | default(false)) and
|
||||
(
|
||||
('An identical record already exists' not in (_cf_call.msg | default('') | string))
|
||||
and
|
||||
('81058' not in (_cf_call.msg | default('') | string))
|
||||
)
|
||||
|
||||
- name: Apply CNAME/MX/TXT
|
||||
community.general.cloudflare_dns:
|
||||
api_token: "{{ CLOUDFLARE_API_TOKEN }}"
|
||||
zone: "{{ item.zone }}"
|
||||
type: "{{ item.type }}"
|
||||
name: "{{ item.name }}"
|
||||
value: "{{ item.value }}"
|
||||
ttl: "{{ item.ttl | default(1) }}"
|
||||
priority: "{{ (item.type == 'MX') | ternary(item.priority | default(10), omit) }}"
|
||||
state: "{{ item.state | default('present') }}"
|
||||
loop: "{{ cloudflare_records | selectattr('type','in',['CNAME','MX','TXT']) | list }}"
|
||||
loop_control: { label: "{{ item.type }} {{ item.name }} -> {{ item.value }}" }
|
||||
async: "{{ cloudflare_async_enabled | ternary(cloudflare_async_time, omit) }}"
|
||||
poll: "{{ cloudflare_async_enabled | ternary(cloudflare_async_poll, omit) }}"
|
||||
no_log: "{{ cloudflare_no_log | bool }}"
|
||||
register: _cf_call
|
||||
failed_when: >
|
||||
_cf_call is failed and
|
||||
(
|
||||
('An identical record already exists' not in (_cf_call.msg | default('') | string))
|
||||
and
|
||||
('81058' not in (_cf_call.msg | default('') | string))
|
||||
)
|
||||
changed_when: >
|
||||
(_cf_call.changed | default(false)) and
|
||||
(
|
||||
('An identical record already exists' not in (_cf_call.msg | default('') | string))
|
||||
and
|
||||
('81058' not in (_cf_call.msg | default('') | string))
|
||||
)
|
||||
|
||||
- name: Apply SRV
|
||||
community.general.cloudflare_dns:
|
||||
api_token: "{{ CLOUDFLARE_API_TOKEN }}"
|
||||
zone: "{{ item.zone }}"
|
||||
type: SRV
|
||||
service: "{{ item.service }}"
|
||||
proto: "{{ item.proto }}"
|
||||
name: "{{ item.name }}"
|
||||
priority: "{{ item.priority }}"
|
||||
weight: "{{ item.weight }}"
|
||||
port: "{{ item.port }}"
|
||||
value: "{{ item.value }}"
|
||||
ttl: "{{ item.ttl | default(1) }}"
|
||||
state: "{{ item.state | default('present') }}"
|
||||
loop: "{{ cloudflare_records | selectattr('type','equalto','SRV') | list }}"
|
||||
loop_control: { label: "SRV {{ item.service }}.{{ item.proto }} {{ item.name }} -> {{ item.value }}:{{ item.port }}" }
|
||||
ignore_errors: "{{ item.ignore_errors | default(true) }}"
|
||||
async: "{{ cloudflare_async_enabled | ternary(cloudflare_async_time, omit) }}"
|
||||
poll: "{{ cloudflare_async_enabled | ternary(cloudflare_async_poll, omit) }}"
|
||||
no_log: "{{ cloudflare_no_log | bool }}"
|
||||
register: _cf_call
|
||||
failed_when: >
|
||||
_cf_call is failed and
|
||||
(
|
||||
('An identical record already exists' not in (_cf_call.msg | default('') | string))
|
||||
and
|
||||
('81058' not in (_cf_call.msg | default('') | string))
|
||||
)
|
||||
changed_when: >
|
||||
(_cf_call.changed | default(false)) and
|
||||
(
|
||||
('An identical record already exists' not in (_cf_call.msg | default('') | string))
|
||||
and
|
||||
('81058' not in (_cf_call.msg | default('') | string))
|
||||
)
|
Reference in New Issue
Block a user