diff --git a/Makefile b/Makefile index 1e804d7..bded0ed 100644 --- a/Makefile +++ b/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)" diff --git a/README.md b/README.md index d789db2..d094575 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,9 @@ make role lint # Проверка ролей # Проверка всего проекта make lint # Проверить весь проект на ошибки +make check-secrets # Проверить безопасность секретов +make idempotence # Проверить идемпотентность +make chaos # Запустить Chaos Engineering тесты # Управление Vault make vault show # Показать содержимое diff --git a/files/playbooks/chaos.yml b/files/playbooks/chaos.yml new file mode 100644 index 0000000..ae150fe --- /dev/null +++ b/files/playbooks/chaos.yml @@ -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 diff --git a/molecule/universal/converge.yml b/molecule/universal/converge.yml index 6c5804b..15951ef 100644 --- a/molecule/universal/converge.yml +++ b/molecule/universal/converge.yml @@ -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 diff --git a/molecule/universal/create.yml b/molecule/universal/create.yml index 59ac4e2..ad2dbbe 100644 --- a/molecule/universal/create.yml +++ b/molecule/universal/create.yml @@ -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: diff --git a/molecule/universal/molecule.yml b/molecule/universal/molecule.yml index a0b4a65..7ee7794 100644 --- a/molecule/universal/molecule.yml +++ b/molecule/universal/molecule.yml @@ -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 diff --git a/molecule/universal/verify.yml b/molecule/universal/verify.yml index 0aaae85..ce4747d 100644 --- a/molecule/universal/verify.yml +++ b/molecule/universal/verify.yml @@ -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: diff --git a/scripts/secret_scan.sh b/scripts/secret_scan.sh new file mode 100644 index 0000000..26def27 --- /dev/null +++ b/scripts/secret_scan.sh @@ -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 "✅ Все проверки безопасности пройдены!"