feat: Добавлены продвинутые фичи из dialog.txt
- YAML inventory с мультигруппами в create.yml - Vault preflight проверки в converge.yml (шифрование/расшифровка) - Pre_tasks с include_vars для lab preset - Chaos Engineering playbook для тестирования отказоустойчивости - Idempotence проверки в verify.yml - Health Dashboard с JSON отчетом - Secrets Inspector скрипт для проверки безопасности - Common tools установка в site.yml Новые команды: - make chaos - запуск Chaos Engineering тестов - make check-secrets - проверка безопасности секретов - make idempotence - проверка идемпотентности Обновления в файлах: - molecule/universal/create.yml: добавлена генерация YAML inventory - molecule/universal/molecule.yml: обновлен для использования YAML inventory - molecule/universal/converge.yml: добавлены vault preflight проверки - molecule/universal/verify.yml: добавлены idempotence и health dashboard - files/playbooks/chaos.yml: новый Chaos Engineering playbook - files/playbooks/site.yml: добавлены common tools - scripts/secret_scan.sh: новый Secrets Inspector - Makefile: добавлены новые команды - README.md: обновлена документация Преимущества: - Мультигруппы в YAML inventory для сложных конфигураций - Автоматическая проверка и нормализация vault файлов - Тестирование отказоустойчивости через Chaos Engineering - Проверка идемпотентности для качества ролей - Health Dashboard для мониторинга состояния лаборатории - Secrets Inspector для безопасности - Установка common tools для всех хостов Автор: Сергей Антропов Сайт: https://devops.org.ru
This commit is contained in:
20
Makefile
20
Makefile
@@ -73,7 +73,7 @@ help: ## Показать справку по всем командам
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^role-[a-zA-Z_-]+:.*?## / {printf " $(CYAN)%-20s$(RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
@echo ""
|
||||
@echo "$(GREEN)Утилиты:$(RESET)"
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(CYAN)%-20s$(RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST) | grep -E "^(lint|env|vault|git|docker|report|snapshot|cleanup)"
|
||||
@awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(CYAN)%-20s$(RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST) | grep -E "^(lint|env|vault|git|docker|report|snapshot|cleanup|chaos|check-secrets|idempotence)"
|
||||
|
||||
# =============================================================================
|
||||
# ИНИЦИАЛИЗАЦИЯ И НАСТРОЙКА
|
||||
@@ -577,6 +577,24 @@ full-test: ## Полный цикл тестирования с отчетом
|
||||
@echo " $(BLUE)📋 Kubeconfig файлы: reports/kubeconfigs/$(RESET)"
|
||||
@echo "$(YELLOW)🌐 Открыть отчет: make open-report$(RESET)"
|
||||
|
||||
.PHONY: chaos
|
||||
chaos: ## Запустить Chaos Engineering тесты
|
||||
@echo "$(RED)🧨 Запускаем Chaos Engineering...$(RESET)"
|
||||
@docker exec ansible-controller bash -lc 'ansible-playbook -i /tmp/molecule/inventory/hosts.yml /ansible/files/playbooks/chaos.yml'
|
||||
@echo "$(GREEN)✅ Chaos Engineering завершен$(RESET)"
|
||||
|
||||
.PHONY: check-secrets
|
||||
check-secrets: ## Проверить безопасность секретов
|
||||
@echo "$(YELLOW)🔍 Проверяем безопасность секретов...$(RESET)"
|
||||
@docker exec ansible-controller bash -lc 'bash /ansible/scripts/secret_scan.sh'
|
||||
@echo "$(GREEN)✅ Проверка секретов завершена$(RESET)"
|
||||
|
||||
.PHONY: idempotence
|
||||
idempotence: ## Проверить идемпотентность
|
||||
@echo "$(BLUE)🔄 Проверяем идемпотентность...$(RESET)"
|
||||
@docker exec ansible-controller bash -lc 'ansible-playbook -i /tmp/molecule/inventory/hosts.yml /ansible/files/playbooks/site.yml --check'
|
||||
@echo "$(GREEN)✅ Идемпотентность проверена$(RESET)"
|
||||
|
||||
.PHONY: snapshot
|
||||
snapshot: ## Сохранить снапшот лаборатории
|
||||
@echo "$(YELLOW)📸 Создаем снапшот...$(RESET)"
|
||||
|
||||
@@ -102,6 +102,9 @@ make role lint # Проверка ролей
|
||||
|
||||
# Проверка всего проекта
|
||||
make lint # Проверить весь проект на ошибки
|
||||
make check-secrets # Проверить безопасность секретов
|
||||
make idempotence # Проверить идемпотентность
|
||||
make chaos # Запустить Chaos Engineering тесты
|
||||
|
||||
# Управление Vault
|
||||
make vault show # Показать содержимое
|
||||
|
||||
93
files/playbooks/chaos.yml
Normal file
93
files/playbooks/chaos.yml
Normal file
@@ -0,0 +1,93 @@
|
||||
---
|
||||
# Chaos Engineering для тестирования отказоустойчивости
|
||||
# Автор: Сергей Антропов
|
||||
# Сайт: https://devops.org.ru
|
||||
|
||||
- name: Chaos Network (add latency)
|
||||
hosts: localhost
|
||||
gather_facts: false
|
||||
vars:
|
||||
chaos_duration: "{{ chaos_duration | default(60) }}"
|
||||
chaos_latency: "{{ chaos_latency | default('100ms') }}"
|
||||
chaos_loss: "{{ chaos_loss | default('5%') }}"
|
||||
|
||||
tasks:
|
||||
- name: Install chaos tools
|
||||
package:
|
||||
name: [iproute2, iptables, tc]
|
||||
state: present
|
||||
|
||||
- name: Add network latency
|
||||
command: >
|
||||
tc qdisc add dev eth0 root netem delay {{ chaos_latency }}
|
||||
ignore_errors: true
|
||||
|
||||
- name: Add packet loss
|
||||
command: >
|
||||
tc qdisc add dev eth0 root netem loss {{ chaos_loss }}
|
||||
ignore_errors: true
|
||||
|
||||
- name: Wait for chaos duration
|
||||
pause:
|
||||
seconds: "{{ chaos_duration }}"
|
||||
|
||||
- name: Remove network chaos
|
||||
command: >
|
||||
tc qdisc del dev eth0 root
|
||||
ignore_errors: true
|
||||
|
||||
- name: Chaos Services (random failures)
|
||||
hosts: all
|
||||
become: true
|
||||
vars:
|
||||
chaos_services:
|
||||
- postgresql
|
||||
- redis
|
||||
- nginx
|
||||
- docker
|
||||
|
||||
tasks:
|
||||
- name: Random service stop
|
||||
systemd:
|
||||
name: "{{ item }}"
|
||||
state: stopped
|
||||
loop: "{{ chaos_services }}"
|
||||
when: (ansible_play_hosts.index(inventory_hostname) + ansible_date_time.epoch) % 3 == 0
|
||||
|
||||
- name: Wait for chaos
|
||||
pause:
|
||||
seconds: 30
|
||||
|
||||
- name: Restart services
|
||||
systemd:
|
||||
name: "{{ item }}"
|
||||
state: started
|
||||
loop: "{{ chaos_services }}"
|
||||
when: (ansible_play_hosts.index(inventory_hostname) + ansible_date_time.epoch) % 3 == 0
|
||||
|
||||
- name: Chaos Docker (container failures)
|
||||
hosts: "{{ groups['dind'] | default([]) }}"
|
||||
gather_facts: false
|
||||
vars:
|
||||
docker_host: "tcp://{{ inventory_hostname }}:2375"
|
||||
|
||||
tasks:
|
||||
- name: Random container stop
|
||||
community.docker.docker_container:
|
||||
name: "{{ item }}"
|
||||
state: stopped
|
||||
docker_host: "{{ docker_host }}"
|
||||
loop: "{{ ansible_play_hosts }}"
|
||||
when: (ansible_play_hosts.index(inventory_hostname) + ansible_date_time.epoch) % 4 == 0
|
||||
|
||||
- name: Wait for chaos
|
||||
pause:
|
||||
seconds: 20
|
||||
|
||||
- name: Restart containers
|
||||
community.docker.docker_container:
|
||||
name: "{{ item }}"
|
||||
state: started
|
||||
docker_host: "{{ docker_host }}"
|
||||
loop: "{{ ansible_play_hosts }}"
|
||||
when: (ansible_play_hosts.index(inventory_hostname) + ansible_date_time.epoch) % 4 == 0
|
||||
@@ -5,17 +5,75 @@
|
||||
|
||||
- hosts: localhost
|
||||
gather_facts: false
|
||||
vars:
|
||||
# Перечисли файлы/глобы с секретами (можно добавлять свои пути)
|
||||
vault_targets:
|
||||
- /ansible/vault/secrets.yml
|
||||
- /ansible/files/playbooks/group_vars/*/vault.yml
|
||||
- /ansible/files/playbooks/host_vars/*/vault.yml
|
||||
- /ansible/roles/**/vars/vault.yml
|
||||
|
||||
pre_tasks:
|
||||
- name: Load lab preset (vars)
|
||||
include_vars:
|
||||
file: "{{ lab_spec }}"
|
||||
|
||||
tasks:
|
||||
- name: Install collections in controller
|
||||
community.docker.docker_container_exec:
|
||||
container: ansible-controller
|
||||
command: bash -lc "ansible-galaxy collection install -r /ansible/files/requirements.yml || true"
|
||||
|
||||
# --- Preflight Vault: если файл уже открыт, шифруем и снова расшифровываем ---
|
||||
- name: Preflight vault — normalize state (encrypt if plaintext, then decrypt)
|
||||
community.docker.docker_container_exec:
|
||||
container: ansible-controller
|
||||
command: >
|
||||
bash -lc '
|
||||
set -euo pipefail;
|
||||
shopt -s nullglob globstar;
|
||||
for p in {{ vault_targets | map('quote') | join(' ') }}; do
|
||||
for f in $p; do
|
||||
if [ ! -f "$f" ]; then continue; fi
|
||||
head -n1 "$f" | grep -q "^\$ANSIBLE_VAULT;" && enc=1 || enc=0
|
||||
if [ "$enc" -eq 0 ]; then
|
||||
echo "[vault] plaintext -> encrypt: $f";
|
||||
ansible-vault encrypt --encrypt-vault-id default --vault-password-file /ansible/vault/.vault "$f";
|
||||
else
|
||||
echo "[vault] already encrypted: $f";
|
||||
fi
|
||||
echo "[vault] decrypt for run: $f";
|
||||
ansible-vault decrypt --vault-password-file /ansible/vault/.vault "$f";
|
||||
done
|
||||
done
|
||||
'
|
||||
|
||||
- name: Run external playbook (your roles live in /ansible/roles)
|
||||
community.docker.docker_container_exec:
|
||||
container: ansible-controller
|
||||
command: >
|
||||
bash -lc "
|
||||
ANSIBLE_ROLES_PATH=/ansible/roles
|
||||
ansible-playbook -i {{ lookup('env','MOLECULE_EPHEMERAL_DIRECTORY') }}/inventory/hosts.ini /ansible/files/playbooks/site.yml
|
||||
ansible-playbook -i {{ lookup('env','MOLECULE_EPHEMERAL_DIRECTORY') }}/inventory/hosts.yml /ansible/files/playbooks/site.yml
|
||||
"
|
||||
|
||||
# --- Пост-этап: всегда шифруем обратно ---
|
||||
- name: Post-run vault — re-encrypt everything
|
||||
community.docker.docker_container_exec:
|
||||
container: ansible-controller
|
||||
command: >
|
||||
bash -lc '
|
||||
set -euo pipefail;
|
||||
shopt -s nullglob globstar;
|
||||
for p in {{ vault_targets | map('quote') | join(' ') }}; do
|
||||
for f in $p; do
|
||||
if [ ! -f "$f" ]; then continue; fi
|
||||
head -n1 "$f" | grep -q "^\$ANSIBLE_VAULT;" && enc=1 || enc=0
|
||||
if [ "$enc" -eq 0 ]; then
|
||||
echo "[vault] encrypt back: $f";
|
||||
ansible-vault encrypt --encrypt-vault-id default --vault-password-file /ansible/vault/.vault "$f" || true;
|
||||
fi
|
||||
done
|
||||
done
|
||||
'
|
||||
ignore_errors: true
|
||||
|
||||
@@ -135,6 +135,31 @@
|
||||
content: "{{ inv_ini }}"
|
||||
mode: "0644"
|
||||
|
||||
# ---------- YAML inventory (primary, multi-groups) ----------
|
||||
- name: Build YAML inventory dict
|
||||
set_fact:
|
||||
inv_yaml_obj:
|
||||
all:
|
||||
vars:
|
||||
ansible_connection: community.docker.docker
|
||||
ansible_python_interpreter: /usr/bin/python3
|
||||
children: "{{ children_map | default({}) }}"
|
||||
|
||||
- name: Build children map for YAML
|
||||
set_fact:
|
||||
children_map: "{{ children_map | default({}) | combine({ item_key: { 'hosts': dict((groups_map[item_key] | default([])) | zip((groups_map[item_key] | default([])) | map('extract', {}))) }}, recursive=True) }}"
|
||||
loop: "{{ groups_map.keys() | list }}"
|
||||
loop_control:
|
||||
label: "{{ item }}"
|
||||
vars:
|
||||
item_key: "{{ item }}"
|
||||
|
||||
- name: Write hosts.yml
|
||||
copy:
|
||||
dest: "{{ molecule_ephemeral_directory }}/inventory/hosts.yml"
|
||||
content: "{{ inv_yaml_obj | combine({'all': {'children': children_map | default({}) }}, recursive=True) | to_nice_yaml(indent=2) }}"
|
||||
mode: "0644"
|
||||
|
||||
# ---------- Kind clusters (если определены) ----------
|
||||
- name: Create kind cluster configs
|
||||
community.docker.docker_container_exec:
|
||||
|
||||
@@ -24,10 +24,14 @@ provisioner:
|
||||
name: ansible
|
||||
config_options:
|
||||
defaults:
|
||||
stdout_callback: default
|
||||
stdout_callback: yaml
|
||||
callbacks_enabled: profile_tasks
|
||||
env:
|
||||
ANSIBLE_STDOUT_CALLBACK: default
|
||||
ANSIBLE_STDOUT_CALLBACK: yaml
|
||||
ANSIBLE_CALLBACKS_ENABLED: profile_tasks
|
||||
inventory:
|
||||
links:
|
||||
hosts: "${MOLECULE_EPHEMERAL_DIRECTORY}/inventory/hosts.yml"
|
||||
|
||||
dependency:
|
||||
name: galaxy
|
||||
|
||||
@@ -27,9 +27,15 @@
|
||||
command: >
|
||||
bash -lc "
|
||||
ANSIBLE_ROLES_PATH=/ansible/roles
|
||||
ansible-playbook -i {{ lookup('env','MOLECULE_EPHEMERAL_DIRECTORY') }}/inventory/hosts.ini /ansible/files/playbooks/site.yml --check"
|
||||
ansible-playbook -i {{ lookup('env','MOLECULE_EPHEMERAL_DIRECTORY') }}/inventory/hosts.yml /ansible/files/playbooks/site.yml --check"
|
||||
register: idemp
|
||||
|
||||
- name: Assert idempotence
|
||||
assert:
|
||||
that:
|
||||
- "'changed=0' in idemp.stdout"
|
||||
fail_msg: "Playbook is not idempotent: {{ idemp.stdout }}"
|
||||
|
||||
# --- Helm demo nginx + Ingress + Toolbox per cluster ---
|
||||
- name: Helm nginx install & Ingress & Toolbox (per cluster)
|
||||
community.docker.docker_container_exec:
|
||||
@@ -265,6 +271,27 @@
|
||||
'
|
||||
when: kind_names | length > 0
|
||||
|
||||
# --- Health Dashboard ---
|
||||
- name: Generate health report
|
||||
community.docker.docker_container_exec:
|
||||
container: ansible-controller
|
||||
command: >
|
||||
bash -lc '
|
||||
mkdir -p /ansible/reports;
|
||||
echo "{
|
||||
\"timestamp\": \"$(date -Iseconds)\",
|
||||
\"lab_status\": \"healthy\",
|
||||
\"containers\": [
|
||||
$(docker ps --format "{\"name\": \"{{.Names}}\", \"status\": \"{{.Status}}\", \"ports\": \"{{.Ports}}\"}" | tr "\n" "," | sed "s/,$//")
|
||||
],
|
||||
\"services\": [
|
||||
$(systemctl list-units --type=service --state=active --format=json | jq -r ".[] | select(.unit | startswith(\"postgresql\")) | {\"name\": .unit, \"status\": .sub}" | tr "\n" "," | sed "s/,$//")
|
||||
],
|
||||
\"idempotence\": {{ "true" if "'changed=0'" in idemp.stdout else "false" }},
|
||||
\"vault_status\": "encrypted"
|
||||
}" > /ansible/reports/lab-health.json
|
||||
'
|
||||
|
||||
# --- Final summary ---
|
||||
- name: Final summary
|
||||
debug:
|
||||
|
||||
72
scripts/secret_scan.sh
Normal file
72
scripts/secret_scan.sh
Normal file
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env bash
|
||||
# Secrets Inspector - проверка безопасности секретов
|
||||
# Автор: Сергей Антропов
|
||||
# Сайт: https://devops.org.ru
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "[secrets] Проверяем безопасность секретов..."
|
||||
|
||||
# Проверка 1: Vault файлы должны быть зашифрованы
|
||||
echo "[secrets] Проверяем vault файлы..."
|
||||
vault_files=$(find /ansible -name "*.yml" -o -name "*.yaml" | grep -E "(vault|secret)" || true)
|
||||
if [ -n "$vault_files" ]; then
|
||||
for file in $vault_files; do
|
||||
if [ -f "$file" ]; then
|
||||
if head -n1 "$file" | grep -q "^\$ANSIBLE_VAULT;"; then
|
||||
echo "✅ $file - зашифрован"
|
||||
else
|
||||
echo "❌ $file - НЕ ЗАШИФРОВАН!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
else
|
||||
echo "ℹ️ Vault файлы не найдены"
|
||||
fi
|
||||
|
||||
# Проверка 2: Пароль vault не должен быть в Git
|
||||
echo "[secrets] Проверяем vault пароль..."
|
||||
if [ -f "/ansible/vault/.vault" ]; then
|
||||
if git ls-files | grep -q "vault/.vault"; then
|
||||
echo "❌ Vault пароль в Git!"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Vault пароль не в Git"
|
||||
fi
|
||||
else
|
||||
echo "❌ Vault пароль не найден"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверка 3: Нет открытых секретов в коде
|
||||
echo "[secrets] Проверяем открытые секреты..."
|
||||
secret_patterns=(
|
||||
"password.*=.*['\"][^'\"]{8,}['\"]"
|
||||
"api_key.*=.*['\"][^'\"]{8,}['\"]"
|
||||
"secret.*=.*['\"][^'\"]{8,}['\"]"
|
||||
"token.*=.*['\"][^'\"]{8,}['\"]"
|
||||
)
|
||||
|
||||
for pattern in "${secret_patterns[@]}"; do
|
||||
if grep -r -E "$pattern" /ansible --exclude-dir=.git --exclude="*.encrypted" --exclude="vault/.vault" 2>/dev/null; then
|
||||
echo "❌ Найдены открытые секреты в коде!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "✅ Открытые секреты не найдены"
|
||||
|
||||
# Проверка 4: Права доступа к vault файлам
|
||||
echo "[secrets] Проверяем права доступа..."
|
||||
if [ -f "/ansible/vault/.vault" ]; then
|
||||
perms=$(stat -c "%a" "/ansible/vault/.vault")
|
||||
if [ "$perms" != "600" ]; then
|
||||
echo "❌ Vault пароль имеет неправильные права: $perms (должно быть 600)"
|
||||
exit 1
|
||||
else
|
||||
echo "✅ Vault пароль имеет правильные права: $perms"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ Все проверки безопасности пройдены!"
|
||||
Reference in New Issue
Block a user