feat(vhosts): add Alicloud DNS record module and batch sync role

This commit is contained in:
Haitao Pan 2025-11-19 13:20:13 +08:00
parent fae2d7b4d7
commit e4cf88d3c3
16 changed files with 481 additions and 0 deletions

View File

@ -0,0 +1,12 @@
- name: setup OpenResty server
hosts: global-homepage.svc.plus
become: true
vars:
alicloud_dns_domain: "svc.plus"
alicloud_dns_rr: "www"
alicloud_dns_type: "A"
alicloud_dns_value: "1.2.3.4"
alicloud_access_key: "{{ aliyun_ak }}"
alicloud_secret_key: "{{ aliyun_sk }}"
roles:
- role: vhosts/alicloud_dns_record

View File

@ -0,0 +1,16 @@
---
- hosts: localhost
gather_facts: no
# 动态加载 DNS 配置文件
vars_files:
- vars/dns_records_svc_plus.yaml # ← 可以切换成不同环境
# 如果你想在命令行覆盖 AK/SK则可以使用 --extra-vars
vars:
alicloud_access_key: "{{ aliyun_ak | default('') }}"
alicloud_secret_key: "{{ aliyun_sk | default('') }}"
roles:
- role: vhosts/alicloud_dns_sync

View File

@ -0,0 +1,8 @@
- name: setup xcontrol web
hosts: all
become: true
vars:
group: mail
roles:
#- roles/vhosts/common/
- roles/vhosts/nodejs/

View File

@ -0,0 +1 @@
ansible-playbook batch_dns_sync.yml --extra-vars "aliyun_ak=XXXX aliyun_sk=YYYY"

View File

@ -0,0 +1,17 @@
---
- name: Ensure Alicloud DNS Record
alicloud_dns_record:
state: present
domain: "{{ alicloud_dns_domain }}"
rr: "{{ alicloud_dns_rr }}"
type: "{{ alicloud_dns_type }}"
value: "{{ alicloud_dns_value }}"
ttl: "{{ alicloud_dns_ttl }}"
priority: "{{ alicloud_dns_priority }}"
access_key_id: "{{ alicloud_access_key }}"
access_key_secret: "{{ alicloud_secret_key }}"
security_token: "{{ alicloud_security_token }}"
register: dns_result
- debug:
var: dns_result

View File

@ -0,0 +1,167 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
from ansible.module_utils.basic import AnsibleModule
from alibabacloud_alidns20150109.client import Client as Alidns20150109Client
from alibabacloud_credentials.client import Client as CredentialClient
from alibabacloud_tea_openapi import models as open_api_models
from alibabacloud_tea_util import models as util_models
from alibabacloud_alidns20150109 import models as alidns_models
# Build Client (AK/SK 优先 → STS → Credential Chain)
def create_client(access_key_id=None, access_key_secret=None, security_token=None):
if access_key_id and access_key_secret:
config = open_api_models.Config(
access_key_id=access_key_id,
access_key_secret=access_key_secret,
security_token=security_token
)
config.endpoint = "alidns.aliyuncs.com"
return Alidns20150109Client(config)
credential = CredentialClient()
config = open_api_models.Config(credential=credential)
config.endpoint = "alidns.aliyuncs.com"
return Alidns20150109Client(config)
# Helper: find existing record
def find_record(client, domain, rr, record_type):
req = alidns_models.DescribeDomainRecordsRequest(
domain_name=domain,
rr_key_word=rr,
type_key_word=record_type,
page_size=100
)
resp = client.describe_domain_records_with_options(
req, util_models.RuntimeOptions()
)
records = resp.body.domain_records.record or []
for r in records:
if r.rr == rr and r.type == record_type:
return r
return None
def main():
module = AnsibleModule(
argument_spec=dict(
state=dict(type='str', choices=['present', 'absent'], default='present'),
domain=dict(type='str', required=True),
rr=dict(type='str', required=True),
type=dict(type='str', required=True),
value=dict(type='str'),
ttl=dict(type='int', default=600),
priority=dict(type='int'),
# 支持 AK/SK
access_key_id=dict(type='str', no_log=True),
access_key_secret=dict(type='str', no_log=True),
security_token=dict(type='str', no_log=True),
),
supports_check_mode=True
)
state = module.params["state"]
domain = module.params["domain"]
rr = module.params["rr"]
record_type = module.params["type"]
value = module.params["value"]
ttl = module.params["ttl"]
priority = module.params["priority"]
access_key_id = module.params["access_key_id"]
access_key_secret = module.params["access_key_secret"]
security_token = module.params["security_token"]
client = create_client(access_key_id, access_key_secret, security_token)
# Find record
try:
existing = find_record(client, domain, rr, record_type)
except Exception as e:
module.fail_json(msg=f"Failed to query DNS records: {e}")
# ----------------------------
# ABSENT (delete)
# ----------------------------
if state == "absent":
if not existing:
module.exit_json(changed=False, msg="Record already absent")
if module.check_mode:
module.exit_json(changed=True)
try:
req = alidns_models.DeleteDomainRecordRequest(
record_id=existing.record_id
)
client.delete_domain_record_with_options(req, util_models.RuntimeOptions())
except Exception as e:
module.fail_json(msg=f"Failed to delete record: {e}")
module.exit_json(changed=True, msg="Record deleted", record_id=existing.record_id)
# ----------------------------
# PRESENT (create / update)
# ----------------------------
if not value:
module.fail_json(msg="value is required when state=present")
if existing:
need_update = (
existing.value != value or
existing.ttl != ttl or
(priority is not None and existing.priority != priority)
)
if not need_update:
module.exit_json(changed=False, msg="Record already up to date", record_id=existing.record_id)
if module.check_mode:
module.exit_json(changed=True)
try:
req = alidns_models.UpdateDomainRecordRequest(
record_id=existing.record_id,
rr=rr,
type=record_type,
value=value,
ttl=ttl,
priority=priority,
)
client.update_domain_record_with_options(req, util_models.RuntimeOptions())
except Exception as e:
module.fail_json(msg=f"Failed to update record: {e}")
module.exit_json(changed=True, msg="Record updated", record_id=existing.record_id)
# ----------------------------
# CREATE
# ----------------------------
if module.check_mode:
module.exit_json(changed=True)
try:
req = alidns_models.AddDomainRecordRequest(
domain_name=domain,
rr=rr,
type=record_type,
value=value,
ttl=ttl,
priority=priority,
)
resp = client.add_domain_record_with_options(req, util_models.RuntimeOptions())
record_id = resp.body.record_id
except Exception as e:
module.fail_json(msg=f"Failed to create record: {e}")
module.exit_json(changed=True, msg="Record created", record_id=record_id)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,18 @@
---
- name: Ensure Alicloud DNS Record
alicloud_dns_record:
state: present
domain: "{{ alicloud_dns_domain }}"
rr: "{{ alicloud_dns_rr }}"
type: "{{ alicloud_dns_type }}"
value: "{{ alicloud_dns_value }}"
ttl: "{{ alicloud_dns_ttl }}"
priority: "{{ alicloud_dns_priority }}"
access_key_id: "{{ alicloud_access_key }}"
access_key_secret: "{{ alicloud_secret_key }}"
security_token: "{{ alicloud_security_token }}"
register: dns_result
- debug:
var: dns_result

View File

@ -0,0 +1,7 @@
---
alicloud_dns_sync_domain: ""
alicloud_dns_sync_records: []
alicloud_dns_sync_output: "/tmp/dns_records.yaml"
alicloud_access_key: ""
alicloud_secret_key: ""

View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
import sys
import yaml
from alibabacloud_alidns20150109.client import Client
from alibabacloud_tea_openapi import models as open_api_models
def client(ak, sk):
config = open_api_models.Config(
access_key_id=ak,
access_key_secret=sk,
endpoint="alidns.aliyuncs.com",
)
return Client(config)
def sync(domain, records, ak, sk):
c = client(ak, sk)
# get all existing records
resp = c.describe_domain_records(
open_api_models.Config(domain_name=domain)
)
existing = { (i.rr, i.type): i for i in resp.body.domain_records.record }
for rec in records:
key = (rec["rr"], rec["type"])
ttl = rec.get("ttl", 600)
if key not in existing:
print("CREATE:", rec)
c.add_domain_record({
"DomainName": domain,
"RR": rec["rr"],
"Type": rec["type"],
"Value": rec["value"],
"TTL": ttl,
})
else:
cur = existing[key]
if cur.value != rec["value"] or cur.ttl != ttl:
print("UPDATE:", rec)
c.update_domain_record({
"RecordId": cur.record_id,
"RR": rec["rr"],
"Type": rec["type"],
"Value": rec["value"],
"TTL": ttl,
})
if __name__ == "__main__":
fn = sys.argv[1]
ak = sys.argv[2]
sk = sys.argv[3]
cfg = yaml.safe_load(open(fn))
for domain, recs in cfg.items():
sync(domain, recs, ak, sk)

View File

@ -0,0 +1,18 @@
---
- name: Generate DNS records file from template
template:
src: dns_records.yaml.j2
dest: "{{ alicloud_dns_sync_output }}"
- name: Upload dns_sync.py
copy:
src: dns_sync.py
dest: /tmp/dns_sync.py
mode: '0755'
- name: Sync DNS records
command: >
python3 /tmp/dns_sync.py
{{ alicloud_dns_sync_output }}
{{ alicloud_access_key }}
{{ alicloud_secret_key }}

View File

@ -0,0 +1,7 @@
{{ alicloud_dns_sync_domain }}:
{% for rec in alicloud_dns_sync_records %}
- rr: "{{ rec.rr }}"
type: "{{ rec.type }}"
value: "{{ rec.value }}"
ttl: {{ rec.ttl | default(600) }}
{% endfor %}

View File

@ -41,6 +41,20 @@ packages:
- uidmap
- fuse-overlayfs
# S3FS 挂载配置(可选)
s3fs_enable: false
s3fs_config:
bucket: "" # S3 存储桶名称
mount_point: "" # 挂载点路径,例如:/data/update-server/
access_key: "" # AWS Access Key ID
secret_key: "" # AWS Secret Access Key
url: "https://s3.amazonaws.com" # S3 端点 URL
region: "us-east-1" # S3 区域
passwd_file: "~/.passwd-s3fs" # 密码文件路径
use_path_request_style: true # 是否使用路径请求样式
allow_other: true # 是否允许其他用户访问
nonempty: false # 是否允许挂载到非空目录
#config_temp:
# k8s-node:

View File

@ -0,0 +1,85 @@
---
- name: "S3FS | 检查 s3fs 配置"
fail:
msg: "S3FS 需要配置 s3fs_config.bucket 和 s3fs_config.mount_point"
when:
- s3fs_config.bucket | length == 0
- s3fs_config.mount_point | length == 0
- name: "S3FS | 检查 AWS 凭证"
fail:
msg: "S3FS 需要配置 s3fs_config.access_key 和 s3fs_config.secret_key"
when:
- s3fs_config.access_key | length == 0
- s3fs_config.secret_key | length == 0
- name: "S3FS | 安装 s3fs 软件包"
apt:
name: s3fs
state: present
become: yes
when: ansible_facts.os_family == 'Debian'
- name: "S3FS | 安装 s3fs 软件包 (CentOS/RHEL)"
yum:
name: s3fs-fuse
state: present
become: yes
when: ansible_facts.os_family == 'RedHat'
- name: "S3FS | 创建密码文件"
copy:
content: "{{ s3fs_config.access_key }}:{{ s3fs_config.secret_key }}"
dest: "{{ s3fs_config.passwd_file | expanduser }}"
mode: '0600'
owner: root
group: root
when: s3fs_config.access_key | length > 0 and s3fs_config.secret_key | length > 0
- name: "S3FS | 创建挂载点目录"
file:
path: "{{ s3fs_config.mount_point }}"
state: directory
mode: '0755'
owner: root
group: root
- name: "S3FS | 检查是否已挂载"
shell: "mount | grep -q '{{ s3fs_config.mount_point }}' && echo 'mounted' || echo 'not mounted'"
register: s3fs_mount_check
changed_when: false
failed_when: false
- name: "S3FS | 挂载 S3 存储桶"
command: >
s3fs {{ s3fs_config.bucket }} {{ s3fs_config.mount_point }}
-o passwd_file={{ s3fs_config.passwd_file | expanduser }}
{% if s3fs_config.allow_other %}-o allow_other{% endif %}
-o url={{ s3fs_config.url }}
{% if s3fs_config.use_path_request_style %}-o use_path_request_style{% endif %}
args:
creates: "{{ s3fs_config.mount_point }}/.s3fs_configured"
when: s3fs_mount_check.stdout == 'not mounted'
- name: "S3FS | 创建挂载标记文件"
copy:
content: "S3FS mounted at {{ ansible_date_time.iso8601 }}"
dest: "{{ s3fs_config.mount_point }}/.s3fs_configured"
mode: '0644'
owner: root
group: root
when: s3fs_mount_check.stdout == 'not mounted'
- name: "S3FS | 验证挂载"
shell: "mount | grep '{{ s3fs_config.mount_point }}'"
register: s3fs_verify_mount
changed_when: false
failed_when: true
- name: "S3FS | 显示挂载信息"
debug:
msg: |
S3 存储桶已成功挂载!
存储桶: {{ s3fs_config.bucket }}
挂载点: {{ s3fs_config.mount_point }}
状态: {{ s3fs_verify_mount.stdout }}

View File

@ -28,6 +28,11 @@
when: install_packages | bool
tags: [pkgs, baseline]
- name: "Common | S3FS 挂载"
ansible.builtin.include_tasks: configure_s3fs.yml
when: s3fs_enable | bool
tags: [s3fs, mount]
#- name: Include GPU Configuration
# include_tasks: include_gpu.yaml
# when: (ansible_facts['distribution'] == "Ubuntu") or (ansible_facts['distribution'] == "Debian")

View File

@ -0,0 +1,15 @@
alicloud_dns_sync_domain: "svc.plus"
alicloud_dns_sync_records:
- rr: www
type: A
value: 1.1.1.1
ttl: 600
- rr: api
type: A
value: 2.2.2.2
- rr: mx
type: MX
value: mail.svc.plus
ttl: 300

View File

@ -0,0 +1,32 @@
#!/usr/bin/env bash
set -e
NODE_IP="$1"
USER="ubuntu"
ssh $USER@$NODE_IP "
sudo apt purge curl unzip -y
curl -L https://github.com/XTLS/Xray-install/raw/main/install-release.sh -o /tmp/install-release.sh
sudo bash /tmp/install-release.sh
"
scp /etc/ssl/svc.* $USER@$NODE_IP:/tmp/
scp config.json $USER@$NODE_IP:/tmp/
ssh $USER@$NODE_IP "
sudo cp /tmp/svc.* /etc/ssl/
sudo cp /tmp/config.json /usr/local/etc/xray/config.json
sudo chown root:root /etc/ssl/svc.plus.pem
sudo chmod 644 /etc/ssl/svc.plus.pem
sudo chown root:nogroup /etc/ssl/svc.plus.key
sudo chmod 640 /etc/ssl/svc.plus.key
sudo chown root:root /usr/local/etc/xray/config.json
sudo chmod 644 /usr/local/etc/xray/config.json
sudo systemctl restart xray
sudo systemctl status xray
sudo journalctl -fu xray
"