feat: добавить аддон yandex-dns-controller — управление DNS Yandex 360
CronJob (*/5 мин) reconcile ConfigMap → Yandex 360 DNS API.
Safe mode: управляет только записями с managed: true.
Никогда не удаляет неуправляемые записи (MX, DKIM, SPF и т.д.).
Удаление только при двух условиях одновременно:
1. Запись была создана контроллером (есть в state ConfigMap)
2. Запись полностью удалена из ConfigMap (не просто managed: false)
Переключение managed: true → false = release без удаления из DNS.
API: /directory/v1/org/{org_id}/domains/{domain}/dns
Fields: A→content, CNAME→target, TXT→text, MX→exchange+preference
This commit is contained in:
220
addons/yandex-dns-controller/README.md
Normal file
220
addons/yandex-dns-controller/README.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# yandex-dns-controller
|
||||
|
||||
CronJob-контроллер для управления DNS-записями Yandex 360.
|
||||
|
||||
**Safe mode**: управляет только записями с `managed: true`. Все остальные записи игнорируются — никогда не изменяются и не удаляются.
|
||||
|
||||
## Принцип работы
|
||||
|
||||
```
|
||||
ConfigMap (zones.yaml)
|
||||
↓
|
||||
CronJob (*/5 мин)
|
||||
↓
|
||||
controller.py
|
||||
↓
|
||||
Yandex 360 DNS API
|
||||
```
|
||||
|
||||
### Модель управления
|
||||
|
||||
| Запись | Поведение |
|
||||
|---|---|
|
||||
| `managed: true` в ConfigMap | Контроллер добавляет, обновляет, и при полном удалении из конфига — удаляет |
|
||||
| `managed: false` в ConfigMap | Документация только, контроллер не трогает |
|
||||
| Отсутствует в ConfigMap | Игнорируется полностью, никогда не удаляется |
|
||||
|
||||
### Логика удаления (очень важно)
|
||||
|
||||
Запись удаляется из DNS **только при одновременном выполнении двух условий**:
|
||||
1. Запись была создана этим контроллером (есть в state)
|
||||
2. Запись **полностью удалена** из ConfigMap (не просто переключена в `managed: false`)
|
||||
|
||||
Если запись переключается из `managed: true` → `managed: false`, контроллер **перестаёт её отслеживать, но не удаляет** из DNS.
|
||||
|
||||
### State ConfigMap
|
||||
|
||||
Контроллер хранит состояние в ConfigMap `yandex-dns-controller-state`. Это позволяет безопасно удалять записи которые были удалены из конфига.
|
||||
|
||||
```bash
|
||||
# Просмотр текущего состояния
|
||||
kubectl -n yandex-dns-controller get cm yandex-dns-controller-state \
|
||||
-o jsonpath='{.data.state\.json}' | jq .
|
||||
```
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
### 1. Настроить credentials
|
||||
|
||||
```yaml
|
||||
# group_vars/all/vault.yml (зашифровать через ansible-vault)
|
||||
yandex_dns:
|
||||
org_id: "3312086" # https://admin.yandex.ru/company-profile
|
||||
token: "y0_Agk..." # https://oauth.yandex.ru/ (scope: DNS)
|
||||
```
|
||||
|
||||
Как получить токен:
|
||||
1. Создай приложение на https://oauth.yandex.ru/
|
||||
2. Доступ: **Управление записями DNS**
|
||||
3. Открой в браузере: `https://oauth.yandex.ru/authorize?response_type=token&client_id={CLIENT_ID}`
|
||||
4. Скопируй `access_token` со страницы
|
||||
|
||||
### 2. Настроить зоны
|
||||
|
||||
```yaml
|
||||
# group_vars/all/addons.yml
|
||||
addon_yandex_dns_controller: true
|
||||
|
||||
yandex_dns_controller_zones:
|
||||
domains:
|
||||
- name: antropoff.ru
|
||||
|
||||
# 🔐 СИСТЕМНЫЕ — не трогаем (managed: false — только документация)
|
||||
systemRecords:
|
||||
- name: "@"
|
||||
type: MX
|
||||
ttl: 21600
|
||||
value: "mx.yandex.net."
|
||||
priority: 10
|
||||
managed: false
|
||||
|
||||
- name: "@"
|
||||
type: TXT
|
||||
ttl: 21600
|
||||
value: "v=spf1 redirect=_spf.yandex.net"
|
||||
managed: false
|
||||
|
||||
- name: "mail"
|
||||
type: CNAME
|
||||
ttl: 21600
|
||||
value: "domain.mail.yandex.net."
|
||||
managed: false
|
||||
|
||||
# 🌐 ОСНОВНЫЕ A-записи — не трогаем
|
||||
coreRecords:
|
||||
- name: "@"
|
||||
type: A
|
||||
ttl: 3600
|
||||
value: "217.150.201.203"
|
||||
managed: false
|
||||
|
||||
# 🧩 СЕРВИСЫ — не трогаем
|
||||
serviceRecords:
|
||||
- name: "git"
|
||||
type: CNAME
|
||||
ttl: 3600
|
||||
value: "antropoff.ru."
|
||||
managed: false
|
||||
|
||||
# ...остальные CNAME...
|
||||
|
||||
# ✅ УПРАВЛЯЕМ КОНТРОЛЛЕРОМ
|
||||
- name: "k8s-test"
|
||||
type: CNAME
|
||||
ttl: 300
|
||||
value: "antropoff.ru."
|
||||
managed: true
|
||||
|
||||
# Записи которыми управляет K3S кластер
|
||||
records:
|
||||
- name: k8s-ingress
|
||||
type: A
|
||||
ttl: 300
|
||||
value: "192.168.1.100" # kube-vip VIP
|
||||
managed: true
|
||||
```
|
||||
|
||||
### 3. Развернуть
|
||||
|
||||
```bash
|
||||
make addon-yandex-dns-controller
|
||||
```
|
||||
|
||||
## Конфигурация
|
||||
|
||||
| Переменная | По умолчанию | Описание |
|
||||
|---|---|---|
|
||||
| `yandex_dns_controller_namespace` | `yandex-dns-controller` | Namespace |
|
||||
| `yandex_dns_controller_schedule` | `*/5 * * * *` | Расписание CronJob |
|
||||
| `yandex_dns_controller_dry_run` | `false` | Dry-run (не вносить изменения) |
|
||||
| `yandex_dns_controller_image` | `python:3.11-slim` | Docker образ |
|
||||
| `yandex_dns_controller_zones` | `{domains: []}` | Конфигурация зон |
|
||||
|
||||
## Поддерживаемые типы записей
|
||||
|
||||
| Тип | Поле API | Пример value |
|
||||
|---|---|---|
|
||||
| `A` | `content` | `"192.168.1.1"` |
|
||||
| `AAAA` | `content` | `"::1"` |
|
||||
| `CNAME` | `target` | `"antropoff.ru."` |
|
||||
| `TXT` | `text` | `"v=spf1 ..."` |
|
||||
| `MX` | `exchange` + `preference` | `"mx.yandex.net."` + `priority: 10` |
|
||||
| `NS` | `target` | `"ns1.example.com."` |
|
||||
|
||||
## Dry-run режим
|
||||
|
||||
Включить временно без изменения конфига:
|
||||
```bash
|
||||
make addon-yandex-dns-controller ARGS="-e yandex_dns_controller_dry_run=true"
|
||||
```
|
||||
|
||||
Или задать постоянно:
|
||||
```yaml
|
||||
yandex_dns_controller_dry_run: true
|
||||
```
|
||||
|
||||
В dry-run режиме контроллер показывает что бы он сделал, но не вносит изменений в DNS.
|
||||
|
||||
## Ручной запуск
|
||||
|
||||
```bash
|
||||
# Запустить немедленно
|
||||
kubectl create job --from=cronjob/yandex-dns-controller \
|
||||
dns-manual-$(date +%s) -n yandex-dns-controller
|
||||
|
||||
# Наблюдать за логами
|
||||
kubectl -n yandex-dns-controller logs \
|
||||
-l app.kubernetes.io/name=yandex-dns-controller \
|
||||
--tail=100 -f
|
||||
```
|
||||
|
||||
## Диагностика
|
||||
|
||||
```bash
|
||||
# Статус CronJob
|
||||
kubectl -n yandex-dns-controller get cronjob,jobs,pods
|
||||
|
||||
# Логи последнего запуска
|
||||
kubectl -n yandex-dns-controller logs \
|
||||
$(kubectl -n yandex-dns-controller get pods \
|
||||
-l app.kubernetes.io/name=yandex-dns-controller \
|
||||
--sort-by=.metadata.creationTimestamp -o name | tail -1) \
|
||||
-c controller --tail=100
|
||||
|
||||
# Что контролируется (state)
|
||||
kubectl -n yandex-dns-controller get cm yandex-dns-controller-state \
|
||||
-o jsonpath='{.data.state\.json}' | jq .
|
||||
|
||||
# Просмотр текущего ConfigMap зон
|
||||
kubectl -n yandex-dns-controller get cm yandex-dns-zones -o yaml
|
||||
|
||||
# Редактировать зоны вручную (без Ansible)
|
||||
kubectl -n yandex-dns-controller edit cm yandex-dns-zones
|
||||
```
|
||||
|
||||
## Обновление записей через GitOps
|
||||
|
||||
Все изменения DNS делаются через `group_vars/all/addons.yml`:
|
||||
|
||||
1. Добавить/изменить запись в `yandex_dns_controller_zones` с `managed: true`
|
||||
2. Закоммитить и запустить `make addon-yandex-dns-controller`
|
||||
3. Контроллер подхватит изменение на следующем запуске CronJob
|
||||
|
||||
## Защита существующих записей
|
||||
|
||||
Все записи с `managed: false` — это документация. Контроллер **никогда**:
|
||||
- Не создаёт их (даже если они отсутствуют в DNS)
|
||||
- Не обновляет (даже если value отличается)
|
||||
- Не удаляет
|
||||
|
||||
Это гарантирует что MX-записи Яндекс.Почты, DKIM, SPF и другие критичные записи не будут затронуты.
|
||||
7
addons/yandex-dns-controller/playbook.yml
Normal file
7
addons/yandex-dns-controller/playbook.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
- name: Install yandex-dns-controller
|
||||
hosts: k3s_master[0]
|
||||
gather_facts: false
|
||||
become: true
|
||||
roles:
|
||||
- role: "{{ playbook_dir }}/role"
|
||||
14
addons/yandex-dns-controller/role/chart/Chart.yaml
Normal file
14
addons/yandex-dns-controller/role/chart/Chart.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
apiVersion: v2
|
||||
name: yandex-dns-controller
|
||||
description: |
|
||||
CronJob-контроллер для управления DNS-записями Yandex 360.
|
||||
Safe mode: управляет только записями с managed: true.
|
||||
Никогда не удаляет неуправляемые записи.
|
||||
type: application
|
||||
version: 1.0.0
|
||||
appVersion: "1.0.0"
|
||||
keywords:
|
||||
- dns
|
||||
- yandex
|
||||
- yandex360
|
||||
home: https://git.antropoff.ru/DevOpsTools/K3S
|
||||
315
addons/yandex-dns-controller/role/chart/files/controller.py
Normal file
315
addons/yandex-dns-controller/role/chart/files/controller.py
Normal file
@@ -0,0 +1,315 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Yandex 360 DNS Controller — safe mode.
|
||||
|
||||
Rules:
|
||||
ADD — record in ConfigMap (managed:true) but absent from live DNS
|
||||
UPDATE — record in ConfigMap (managed:true) with changed value/ttl
|
||||
DELETE — record was in previous state (was managed) AND is now completely
|
||||
removed from ConfigMap (not just set to managed:false)
|
||||
NEVER — touch any record not tracked in controller state
|
||||
RELEASE— record changed from managed:true → managed:false:
|
||||
stop tracking it, do NOT delete from DNS
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import requests
|
||||
import yaml
|
||||
|
||||
# ─── Configuration ────────────────────────────────────────────────────────────
|
||||
BASE_URL = "https://api360.yandex.net/directory/v1/org"
|
||||
K8S_API = "https://kubernetes.default.svc"
|
||||
K8S_TOKEN = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
||||
K8S_CA = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
|
||||
ZONES_FILE = os.environ.get("ZONES_FILE", "/etc/dns-zones/zones.yaml")
|
||||
STATE_NS = os.environ.get("STATE_NAMESPACE", "yandex-dns-controller")
|
||||
STATE_CM = os.environ.get("STATE_CONFIGMAP", "yandex-dns-controller-state")
|
||||
DRY_RUN = os.environ.get("DRY_RUN", "false").lower() in ("true", "1", "yes")
|
||||
|
||||
# ─── HTTP with retry ──────────────────────────────────────────────────────────
|
||||
|
||||
def _req(method, url, retries=3, **kwargs):
|
||||
last = None
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
r = requests.request(method, url, timeout=30, **kwargs)
|
||||
if r.status_code == 429:
|
||||
wait = 2 ** (attempt + 1)
|
||||
print(f" rate-limited, retry in {wait}s...")
|
||||
time.sleep(wait)
|
||||
continue
|
||||
r.raise_for_status()
|
||||
return r
|
||||
except requests.RequestException as e:
|
||||
last = e
|
||||
if attempt < retries - 1:
|
||||
time.sleep(1)
|
||||
raise last
|
||||
|
||||
# ─── Yandex 360 DNS API ───────────────────────────────────────────────────────
|
||||
# Endpoint: /directory/v1/org/{org_id}/domains/{domain}/dns
|
||||
# Field mapping (from working bash reference):
|
||||
# A/AAAA → content
|
||||
# CNAME → target
|
||||
# TXT → text
|
||||
# MX → exchange + preference
|
||||
# NS → target
|
||||
|
||||
def _url(org_id, domain, record_id=None):
|
||||
base = f"{BASE_URL}/{org_id}/domains/{domain}/dns"
|
||||
return f"{base}/{record_id}" if record_id else base
|
||||
|
||||
def _headers(token):
|
||||
return {"Authorization": f"OAuth {token}", "Content-Type": "application/json"}
|
||||
|
||||
def yandex_list(org_id, domain, token):
|
||||
"""Fetch all DNS records with pagination."""
|
||||
records, page = [], 1
|
||||
while True:
|
||||
data = _req("GET", _url(org_id, domain),
|
||||
headers=_headers(token),
|
||||
params={"page": page, "perPage": 50}).json()
|
||||
records.extend(data.get("records", []))
|
||||
if page >= data.get("pages", 1):
|
||||
break
|
||||
page += 1
|
||||
time.sleep(0.1)
|
||||
return records
|
||||
|
||||
def yandex_create(org_id, domain, token, payload):
|
||||
return _req("POST", _url(org_id, domain),
|
||||
headers=_headers(token), json=payload).json()
|
||||
|
||||
def yandex_update(org_id, domain, token, record_id, payload):
|
||||
return _req("PUT", _url(org_id, domain, record_id),
|
||||
headers=_headers(token), json=payload).json()
|
||||
|
||||
def yandex_delete(org_id, domain, token, record_id):
|
||||
_req("DELETE", _url(org_id, domain, record_id), headers=_headers(token))
|
||||
|
||||
# ─── Record value helpers ─────────────────────────────────────────────────────
|
||||
|
||||
def api_value(rec):
|
||||
"""Extract canonical value from a live Yandex API record."""
|
||||
t = rec.get("type", "")
|
||||
if t in ("A", "AAAA"): return rec.get("content", "")
|
||||
if t in ("CNAME", "NS", "SRV"): return rec.get("target", "")
|
||||
if t == "TXT": return rec.get("text", "")
|
||||
if t == "MX": return rec.get("exchange", "")
|
||||
return ""
|
||||
|
||||
def build_payload(r):
|
||||
"""Build API request payload from a ConfigMap record."""
|
||||
rtype = r["type"]
|
||||
value = r["value"]
|
||||
payload = {"name": r["name"], "type": rtype, "ttl": r.get("ttl", 3600)}
|
||||
if rtype in ("A", "AAAA"): payload["content"] = value
|
||||
elif rtype in ("CNAME", "NS"): payload["target"] = value
|
||||
elif rtype == "TXT": payload["text"] = value
|
||||
elif rtype == "MX":
|
||||
payload["exchange"] = value
|
||||
payload["preference"] = r.get("priority", 10)
|
||||
elif rtype == "SRV": payload["target"] = value
|
||||
return payload
|
||||
|
||||
def _norm(v):
|
||||
"""Normalize for comparison: strip trailing dot and whitespace."""
|
||||
return str(v).rstrip(".").strip()
|
||||
|
||||
def records_equal(desired, live):
|
||||
"""True if desired matches live DNS record."""
|
||||
if _norm(desired["value"]) != _norm(api_value(live)):
|
||||
return False
|
||||
if desired.get("ttl") and int(desired["ttl"]) != int(live.get("ttl", 0)):
|
||||
return False
|
||||
rtype = desired["type"]
|
||||
if rtype == "MX":
|
||||
want_prio = desired.get("priority", 10)
|
||||
if want_prio != live.get("preference"):
|
||||
return False
|
||||
return True
|
||||
|
||||
# ─── Kubernetes state ConfigMap ───────────────────────────────────────────────
|
||||
# State format: {"domain.ru": {"name:type": record_id, ...}, ...}
|
||||
|
||||
def _k8s_session():
|
||||
with open(K8S_TOKEN) as f:
|
||||
tok = f.read().strip()
|
||||
s = requests.Session()
|
||||
s.verify = K8S_CA
|
||||
s.headers["Authorization"] = f"Bearer {tok}"
|
||||
return s
|
||||
|
||||
def load_state(k8s):
|
||||
url = f"{K8S_API}/api/v1/namespaces/{STATE_NS}/configmaps/{STATE_CM}"
|
||||
r = k8s.get(url, timeout=10)
|
||||
if r.status_code == 404:
|
||||
return {}
|
||||
r.raise_for_status()
|
||||
try:
|
||||
return json.loads(r.json().get("data", {}).get("state.json", "{}"))
|
||||
except json.JSONDecodeError:
|
||||
return {}
|
||||
|
||||
def save_state(k8s, state):
|
||||
if DRY_RUN:
|
||||
print(" [dry-run] state not saved")
|
||||
return
|
||||
url = f"{K8S_API}/api/v1/namespaces/{STATE_NS}/configmaps/{STATE_CM}"
|
||||
body = {
|
||||
"apiVersion": "v1", "kind": "ConfigMap",
|
||||
"metadata": {
|
||||
"name": STATE_CM, "namespace": STATE_NS,
|
||||
"labels": {"app.kubernetes.io/managed-by": "yandex-dns-controller"},
|
||||
},
|
||||
"data": {"state.json": json.dumps(state, indent=2, ensure_ascii=False)},
|
||||
}
|
||||
existing = k8s.get(url, timeout=10)
|
||||
if existing.status_code == 404:
|
||||
k8s.post(f"{K8S_API}/api/v1/namespaces/{STATE_NS}/configmaps",
|
||||
json=body, timeout=10).raise_for_status()
|
||||
else:
|
||||
existing.raise_for_status()
|
||||
k8s.put(url, json=body, timeout=10).raise_for_status()
|
||||
|
||||
# ─── Reconcile ────────────────────────────────────────────────────────────────
|
||||
|
||||
def collect_records(domain_cfg):
|
||||
"""Flatten all sections of a domain config into one list."""
|
||||
out = []
|
||||
for section in ("records", "systemRecords", "coreRecords", "serviceRecords"):
|
||||
out.extend(domain_cfg.get(section, []))
|
||||
return out
|
||||
|
||||
def reconcile_domain(org_id, domain, desired_list, live_records, prev_state, token):
|
||||
"""
|
||||
Returns the new state dict for the domain.
|
||||
"""
|
||||
# live records indexed by "name:type"
|
||||
live = {}
|
||||
for rec in live_records:
|
||||
k = f"{rec['name']}:{rec['type']}"
|
||||
live.setdefault(k, []).append(rec)
|
||||
|
||||
# desired managed records (managed: true)
|
||||
desired = {
|
||||
f"{r['name']}:{r['type']}": r
|
||||
for r in desired_list if r.get("managed", False)
|
||||
}
|
||||
|
||||
# all keys present in config in any section (any managed value)
|
||||
all_config_keys = {f"{r['name']}:{r['type']}" for r in desired_list}
|
||||
|
||||
new_state = {}
|
||||
stats = dict(ok=0, created=0, updated=0, deleted=0, released=0)
|
||||
|
||||
# ── Reconcile desired managed records ─────────────────────────────────────
|
||||
for key, rec in desired.items():
|
||||
payload = build_payload(rec)
|
||||
|
||||
if key in live:
|
||||
live_rec = live[key][0]
|
||||
rid = live_rec["recordId"]
|
||||
if records_equal(rec, live_rec):
|
||||
print(f" ✓ {key:<50} {rec['value']}")
|
||||
stats["ok"] += 1
|
||||
else:
|
||||
old_val = api_value(live_rec)
|
||||
print(f" ↻ {key:<50} {old_val!r} → {rec['value']!r}")
|
||||
if not DRY_RUN:
|
||||
yandex_update(org_id, domain, token, rid, payload)
|
||||
stats["updated"] += 1
|
||||
new_state[key] = rid
|
||||
else:
|
||||
print(f" + {key:<50} {rec['value']} [CREATE]")
|
||||
if not DRY_RUN:
|
||||
result = yandex_create(org_id, domain, token, payload)
|
||||
new_state[key] = result["recordId"]
|
||||
else:
|
||||
new_state[key] = -1
|
||||
stats["created"] += 1
|
||||
|
||||
# ── Handle previously managed records that are no longer managed ──────────
|
||||
for key, rid in prev_state.items():
|
||||
if key in desired:
|
||||
continue # already handled above
|
||||
|
||||
if key in all_config_keys:
|
||||
# Still in config, but managed: false now → release without deleting
|
||||
print(f" ↩ {key:<50} [managed:false → released, record preserved in DNS]")
|
||||
stats["released"] += 1
|
||||
# NOT added to new_state → stops tracking
|
||||
else:
|
||||
# Completely removed from ConfigMap → safe to delete
|
||||
print(f" ✗ {key:<50} [removed from ConfigMap → DELETE]")
|
||||
if not DRY_RUN and rid != -1:
|
||||
try:
|
||||
yandex_delete(org_id, domain, token, rid)
|
||||
except Exception as e:
|
||||
print(f" WARN: delete failed (already gone?): {e}")
|
||||
stats["deleted"] += 1
|
||||
# NOT added to new_state
|
||||
|
||||
print(f"\n {stats['ok']} ok {stats['created']} created "
|
||||
f"{stats['updated']} updated {stats['deleted']} deleted "
|
||||
f"{stats['released']} released")
|
||||
|
||||
return new_state
|
||||
|
||||
# ─── Entry point ─────────────────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
token = os.environ.get("YANDEX_TOKEN", "").strip()
|
||||
org_id = os.environ.get("YANDEX_ORG_ID", "").strip()
|
||||
if not token:
|
||||
sys.exit("FATAL: YANDEX_TOKEN is not set")
|
||||
if not org_id:
|
||||
sys.exit("FATAL: YANDEX_ORG_ID is not set")
|
||||
|
||||
if DRY_RUN:
|
||||
print("=" * 60)
|
||||
print("DRY-RUN MODE — no changes will be made to DNS")
|
||||
print("=" * 60)
|
||||
|
||||
with open(ZONES_FILE) as f:
|
||||
config = yaml.safe_load(f)
|
||||
|
||||
k8s = _k8s_session()
|
||||
state = load_state(k8s)
|
||||
|
||||
errors = 0
|
||||
for domain_cfg in config.get("domains", []):
|
||||
domain = domain_cfg["name"]
|
||||
print(f"\n{'='*60}")
|
||||
print(f" Domain : {domain}")
|
||||
print(f"{'='*60}")
|
||||
try:
|
||||
live = yandex_list(org_id, domain, token)
|
||||
desired_list = collect_records(domain_cfg)
|
||||
prev_state = state.get(domain, {})
|
||||
|
||||
managed_n = sum(1 for r in desired_list if r.get("managed", False))
|
||||
print(f" live={len(live)} config={len(desired_list)} managed={managed_n}\n")
|
||||
|
||||
state[domain] = reconcile_domain(
|
||||
org_id, domain, desired_list, live, prev_state, token
|
||||
)
|
||||
except Exception as e:
|
||||
print(f"\n ERROR: {e}")
|
||||
traceback.print_exc()
|
||||
errors += 1
|
||||
|
||||
save_state(k8s, state)
|
||||
|
||||
if errors:
|
||||
print(f"\n✗ {errors} domain(s) failed")
|
||||
sys.exit(1)
|
||||
print("\n✓ All domains reconciled successfully")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
22
addons/yandex-dns-controller/role/chart/templates/NOTES.txt
Normal file
22
addons/yandex-dns-controller/role/chart/templates/NOTES.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
yandex-dns-controller deployed.
|
||||
|
||||
Schedule : {{ .Values.controller.schedule }}
|
||||
Dry-run : {{ .Values.controller.dryRun }}
|
||||
Domains : {{ .Values.zones.domains | len }}
|
||||
|
||||
The controller runs every {{ .Values.controller.schedule }} and reconciles only
|
||||
records marked with managed: true in the ConfigMap.
|
||||
|
||||
Trigger a manual run:
|
||||
kubectl create job --from=cronjob/yandex-dns-controller dns-manual-$(date +%s) \
|
||||
-n {{ .Release.Namespace }}
|
||||
|
||||
Watch logs:
|
||||
kubectl -n {{ .Release.Namespace }} logs -l app.kubernetes.io/name=yandex-dns-controller \
|
||||
--tail=100 -f
|
||||
|
||||
View controller state:
|
||||
kubectl -n {{ .Release.Namespace }} get configmap yandex-dns-controller-state -o jsonpath='{.data.state\.json}' | jq .
|
||||
|
||||
Edit DNS zones (triggers reconcile on next CronJob run):
|
||||
kubectl -n {{ .Release.Namespace }} edit configmap yandex-dns-zones
|
||||
@@ -0,0 +1,10 @@
|
||||
{{- define "yandex-dns-controller.chart" -}}
|
||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
||||
{{- end }}
|
||||
|
||||
{{- define "yandex-dns-controller.labels" -}}
|
||||
helm.sh/chart: {{ include "yandex-dns-controller.chart" . }}
|
||||
app.kubernetes.io/name: {{ .Chart.Name }}
|
||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: yandex-dns-controller-script
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "yandex-dns-controller.labels" . | nindent 4 }}
|
||||
data:
|
||||
controller.py: |
|
||||
{{ .Files.Get "files/controller.py" | indent 4 }}
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: yandex-dns-zones
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "yandex-dns-controller.labels" . | nindent 4 }}
|
||||
data:
|
||||
zones.yaml: |
|
||||
{{ .Values.zones | toYaml | indent 4 }}
|
||||
@@ -0,0 +1,87 @@
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: yandex-dns-controller
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "yandex-dns-controller.labels" . | nindent 4 }}
|
||||
spec:
|
||||
schedule: {{ .Values.controller.schedule | quote }}
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 3
|
||||
failedJobsHistoryLimit: 5
|
||||
jobTemplate:
|
||||
spec:
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "yandex-dns-controller.labels" . | nindent 12 }}
|
||||
spec:
|
||||
serviceAccountName: yandex-dns-controller
|
||||
restartPolicy: OnFailure
|
||||
volumes:
|
||||
- name: zones
|
||||
configMap:
|
||||
name: yandex-dns-zones
|
||||
- name: script
|
||||
configMap:
|
||||
name: yandex-dns-controller-script
|
||||
defaultMode: 0755
|
||||
- name: pip-pkgs
|
||||
emptyDir: {}
|
||||
initContainers:
|
||||
- name: install-deps
|
||||
image: {{ .Values.controller.image }}
|
||||
command:
|
||||
- pip
|
||||
- install
|
||||
- -q
|
||||
- --no-cache-dir
|
||||
- --target
|
||||
- /pip-pkgs
|
||||
- requests
|
||||
- pyyaml
|
||||
volumeMounts:
|
||||
- name: pip-pkgs
|
||||
mountPath: /pip-pkgs
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
containers:
|
||||
- name: controller
|
||||
image: {{ .Values.controller.image }}
|
||||
command: [python, /scripts/controller.py]
|
||||
env:
|
||||
- name: YANDEX_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: yandex-dns-credentials
|
||||
key: token
|
||||
- name: YANDEX_ORG_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: yandex-dns-credentials
|
||||
key: orgId
|
||||
- name: STATE_NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
- name: STATE_CONFIGMAP
|
||||
value: yandex-dns-controller-state
|
||||
- name: DRY_RUN
|
||||
value: {{ .Values.controller.dryRun | toString | quote }}
|
||||
- name: PYTHONPATH
|
||||
value: /pip-pkgs
|
||||
volumeMounts:
|
||||
- name: zones
|
||||
mountPath: /etc/dns-zones
|
||||
- name: script
|
||||
mountPath: /scripts
|
||||
- name: pip-pkgs
|
||||
mountPath: /pip-pkgs
|
||||
resources:
|
||||
{{- toYaml .Values.controller.resources | nindent 16 }}
|
||||
11
addons/yandex-dns-controller/role/chart/templates/role.yaml
Normal file
11
addons/yandex-dns-controller/role/chart/templates/role.yaml
Normal file
@@ -0,0 +1,11 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: yandex-dns-controller
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "yandex-dns-controller.labels" . | nindent 4 }}
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: [configmaps]
|
||||
verbs: [get, create, update, patch]
|
||||
@@ -0,0 +1,15 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: yandex-dns-controller
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "yandex-dns-controller.labels" . | nindent 4 }}
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: yandex-dns-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: yandex-dns-controller
|
||||
namespace: {{ .Release.Namespace }}
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: yandex-dns-credentials
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "yandex-dns-controller.labels" . | nindent 4 }}
|
||||
type: Opaque
|
||||
data:
|
||||
token: {{ .Values.secret.token | b64enc | quote }}
|
||||
orgId: {{ .Values.secret.orgId | b64enc | quote }}
|
||||
@@ -0,0 +1,7 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: yandex-dns-controller
|
||||
namespace: {{ .Release.Namespace }}
|
||||
labels:
|
||||
{{- include "yandex-dns-controller.labels" . | nindent 4 }}
|
||||
23
addons/yandex-dns-controller/role/chart/values.yaml
Normal file
23
addons/yandex-dns-controller/role/chart/values.yaml
Normal file
@@ -0,0 +1,23 @@
|
||||
# Default values — override via group_vars/all/addons.yml → yandex_dns_controller_*
|
||||
|
||||
controller:
|
||||
image: python:3.11-slim
|
||||
schedule: "*/5 * * * *" # every 5 minutes
|
||||
dryRun: false
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
|
||||
secret:
|
||||
orgId: ""
|
||||
token: ""
|
||||
|
||||
# Full zones config — mirrors the zones.yaml ConfigMap content.
|
||||
# All sections (records, systemRecords, coreRecords, serviceRecords) are supported.
|
||||
# Controller only acts on records with managed: true.
|
||||
zones:
|
||||
domains: []
|
||||
43
addons/yandex-dns-controller/role/defaults/main.yml
Normal file
43
addons/yandex-dns-controller/role/defaults/main.yml
Normal file
@@ -0,0 +1,43 @@
|
||||
---
|
||||
# ─── Helm release ─────────────────────────────────────────────────────────────
|
||||
yandex_dns_controller_namespace: "yandex-dns-controller"
|
||||
yandex_dns_controller_release_name: "yandex-dns-controller"
|
||||
|
||||
# ─── CronJob settings ─────────────────────────────────────────────────────────
|
||||
yandex_dns_controller_schedule: "*/5 * * * *"
|
||||
yandex_dns_controller_dry_run: false
|
||||
yandex_dns_controller_image: "python:3.11-slim"
|
||||
|
||||
# ─── API credentials (задаются в vault.yml) ───────────────────────────────────
|
||||
# yandex_dns:
|
||||
# org_id: "3312086" # ID организации: https://admin.yandex.ru/company-profile
|
||||
# token: "y0_..." # OAuth-токен: https://oauth.yandex.ru/
|
||||
|
||||
# ─── DNS zones ────────────────────────────────────────────────────────────────
|
||||
# Полное содержимое zones.yaml.
|
||||
# Контроллер обрабатывает все секции: records, systemRecords, coreRecords,
|
||||
# serviceRecords. Управляются только записи с managed: true.
|
||||
#
|
||||
# Пример:
|
||||
# yandex_dns_controller_zones:
|
||||
# domains:
|
||||
# - name: example.ru
|
||||
#
|
||||
# # НЕ ТРОГАЕМ — только документация
|
||||
# systemRecords:
|
||||
# - name: "@"
|
||||
# type: MX
|
||||
# ttl: 21600
|
||||
# value: "mx.yandex.net."
|
||||
# priority: 10
|
||||
# managed: false
|
||||
#
|
||||
# # УПРАВЛЯЕМ контроллером
|
||||
# records:
|
||||
# - name: k8s-ingress
|
||||
# type: A
|
||||
# ttl: 300
|
||||
# value: "192.168.1.100"
|
||||
# managed: true
|
||||
yandex_dns_controller_zones:
|
||||
domains: []
|
||||
135
addons/yandex-dns-controller/role/tasks/main.yml
Normal file
135
addons/yandex-dns-controller/role/tasks/main.yml
Normal file
@@ -0,0 +1,135 @@
|
||||
---
|
||||
# ── Validate inputs ───────────────────────────────────────────────────────────
|
||||
|
||||
- name: Validate yandex_dns credentials
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- yandex_dns is defined
|
||||
- yandex_dns.org_id is defined and yandex_dns.org_id | length > 0
|
||||
- yandex_dns.token is defined and yandex_dns.token | length > 0
|
||||
fail_msg: >
|
||||
yandex_dns.org_id and yandex_dns.token must be set in vault.yml.
|
||||
Get org_id from https://admin.yandex.ru/company-profile
|
||||
Get token from https://oauth.yandex.ru/
|
||||
|
||||
- name: Validate yandex_dns_controller_zones has at least one domain
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- yandex_dns_controller_zones.domains is defined
|
||||
- yandex_dns_controller_zones.domains | length > 0
|
||||
fail_msg: >
|
||||
yandex_dns_controller_zones.domains is empty.
|
||||
Define at least one domain in group_vars/all/addons.yml.
|
||||
|
||||
# ── Create namespace ──────────────────────────────────────────────────────────
|
||||
|
||||
- name: Create yandex-dns-controller namespace
|
||||
ansible.builtin.command: >
|
||||
k3s kubectl create namespace {{ yandex_dns_controller_namespace }}
|
||||
--dry-run=client -o yaml | k3s kubectl apply -f -
|
||||
become: true
|
||||
changed_when: false
|
||||
|
||||
# ── Copy Helm chart to master node ───────────────────────────────────────────
|
||||
|
||||
- name: Ensure chart temp directory is clean
|
||||
ansible.builtin.file:
|
||||
path: /tmp/yandex-dns-controller-chart
|
||||
state: absent
|
||||
become: true
|
||||
|
||||
- name: Create chart temp directory
|
||||
ansible.builtin.file:
|
||||
path: /tmp/yandex-dns-controller-chart
|
||||
state: directory
|
||||
mode: "0755"
|
||||
become: true
|
||||
|
||||
- name: Copy Helm chart to master
|
||||
ansible.builtin.copy:
|
||||
src: "{{ role_path }}/chart/"
|
||||
dest: /tmp/yandex-dns-controller-chart/
|
||||
mode: preserve
|
||||
become: true
|
||||
|
||||
# ── Template Helm values ──────────────────────────────────────────────────────
|
||||
|
||||
- name: Template Helm values
|
||||
ansible.builtin.template:
|
||||
src: values.yaml.j2
|
||||
dest: /tmp/yandex-dns-controller-values.yaml
|
||||
mode: "0600"
|
||||
become: true
|
||||
no_log: true # contains OAuth token
|
||||
|
||||
# ── Lint chart ────────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Lint Helm chart
|
||||
ansible.builtin.command: >
|
||||
helm lint /tmp/yandex-dns-controller-chart
|
||||
--values /tmp/yandex-dns-controller-values.yaml
|
||||
become: true
|
||||
changed_when: false
|
||||
register: _helm_lint
|
||||
failed_when: _helm_lint.rc != 0
|
||||
|
||||
# ── Deploy chart ──────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Deploy yandex-dns-controller via Helm
|
||||
ansible.builtin.command: >
|
||||
helm upgrade --install {{ yandex_dns_controller_release_name }}
|
||||
/tmp/yandex-dns-controller-chart
|
||||
--namespace {{ yandex_dns_controller_namespace }}
|
||||
--values /tmp/yandex-dns-controller-values.yaml
|
||||
--atomic
|
||||
--wait
|
||||
--timeout 120s
|
||||
become: true
|
||||
register: _helm_result
|
||||
changed_when: true
|
||||
|
||||
# ── Cleanup temp values file (contains token) ─────────────────────────────────
|
||||
|
||||
- name: Remove temp values file
|
||||
ansible.builtin.file:
|
||||
path: /tmp/yandex-dns-controller-values.yaml
|
||||
state: absent
|
||||
become: true
|
||||
|
||||
# ── Verify ────────────────────────────────────────────────────────────────────
|
||||
|
||||
- name: Get CronJob status
|
||||
ansible.builtin.command: >
|
||||
k3s kubectl -n {{ yandex_dns_controller_namespace }} get cronjob -o wide
|
||||
become: true
|
||||
changed_when: false
|
||||
register: _cronjob_status
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
|
||||
- name: "=== yandex-dns-controller Ready ==="
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "╔══════════════════════════════════════════════════════════════╗"
|
||||
- "║ Yandex 360 DNS Controller — Deployed ║"
|
||||
- "╚══════════════════════════════════════════════════════════════╝"
|
||||
- ""
|
||||
- " Namespace : {{ yandex_dns_controller_namespace }}"
|
||||
- " Schedule : {{ yandex_dns_controller_schedule }}"
|
||||
- " Dry-run : {{ yandex_dns_controller_dry_run }}"
|
||||
- " Domains : {{ yandex_dns_controller_zones.domains | map(attribute='name') | list | join(', ') }}"
|
||||
- ""
|
||||
- " CronJob:"
|
||||
- "{{ _cronjob_status.stdout_lines | to_yaml }}"
|
||||
- ""
|
||||
- " Manual trigger:"
|
||||
- " kubectl create job --from=cronjob/yandex-dns-controller dns-manual-1 \\"
|
||||
- " -n {{ yandex_dns_controller_namespace }}"
|
||||
- ""
|
||||
- " Logs:"
|
||||
- " kubectl -n {{ yandex_dns_controller_namespace }} logs \\"
|
||||
- " -l app.kubernetes.io/name=yandex-dns-controller --tail=100 -f"
|
||||
- ""
|
||||
- " State:"
|
||||
- " kubectl -n {{ yandex_dns_controller_namespace }} get cm yandex-dns-controller-state \\"
|
||||
- " -o jsonpath='{.data.state\\.json}' | jq ."
|
||||
15
addons/yandex-dns-controller/role/templates/values.yaml.j2
Normal file
15
addons/yandex-dns-controller/role/templates/values.yaml.j2
Normal file
@@ -0,0 +1,15 @@
|
||||
# Generated by Ansible — do not edit manually.
|
||||
# Configure via: group_vars/all/addons.yml → yandex_dns_controller_* variables.
|
||||
# Credentials from vault.yml → yandex_dns.org_id / yandex_dns.token
|
||||
|
||||
controller:
|
||||
image: {{ yandex_dns_controller_image | quote }}
|
||||
schedule: {{ yandex_dns_controller_schedule | quote }}
|
||||
dryRun: {{ yandex_dns_controller_dry_run | string | lower }}
|
||||
|
||||
secret:
|
||||
orgId: {{ yandex_dns.org_id | quote }}
|
||||
token: {{ yandex_dns.token | quote }}
|
||||
|
||||
zones:
|
||||
{{ yandex_dns_controller_zones | to_yaml | indent(2, True) }}
|
||||
Reference in New Issue
Block a user