D4.4 Πρακτικό Full Stack με Docker Compose 🚀

Τι θα φτιάξουμε

┌─────────────────────────────────────────────────────────┐
│  Full Stack: WordPress + MariaDB + Redis + Nginx        │
│                                                         │
│  Internet :80                                           │
│      │                                                  │
│      ▼                                                  │
│  ┌─────────┐                                            │
│  │  Nginx  │ ← Reverse proxy + SSL termination          │
│  └────┬────┘                                            │
│       │ :9000 (PHP-FPM)                                 │
│       ▼                                                 │
│  ┌─────────┐    ┌─────────┐    ┌─────────┐              │
│  │WordPress│───►│ MariaDB │    │  Redis  │              │
│  │ (PHP)   │    │  :3306  │    │  :6379  │              │
│  └─────────┘    └─────────┘    └─────────┘              │
│                                                         │
│  Γιατί WordPress;                                       │
│  ✅ Real-world παράδειγμα                               │
│  ✅ PHP + MySQL + Redis — κλασικό stack                 │
│  ✅ Γνωστό σε όλους                                     │
└─────────────────────────────────────────────────────────┘

Δομή Project

# Δημιουργία δομής
mkdir -p ~/ansible/roles/wordpress/{tasks,templates,defaults,handlers}

tree ~/ansible/roles/wordpress/
# roles/wordpress/
# ├── defaults/
# │   └── main.yml
# ├── handlers/
# │   └── main.yml
# ├── tasks/
# │   ├── main.yml
# │   ├── setup.yml
# │   ├── deploy.yml
# │   └── verify.yml
# └── templates/
#     ├── docker-compose.yml.j2
#     ├── nginx.conf.j2
#     └── wp.env.j2

defaults/main.yml

cat > ~/ansible/roles/wordpress/defaults/main.yml << 'EOF'
---
# ============================================================
# WordPress Stack Defaults
# ============================================================

# ── Stack Info ────────────────────────────────
stack_name:    wordpress
stack_version: "1.0.0"
app_env:       production

# ── Paths ─────────────────────────────────────
stack_base_dir:   "/opt/{{ stack_name }}"
stack_config_dir: "/opt/{{ stack_name }}/config"
stack_data_dir:   "/opt/{{ stack_name }}/data"
stack_logs_dir:   "/opt/{{ stack_name }}/logs"

# ── WordPress ─────────────────────────────────
wp_image:       wordpress:6.4-php8.2-fpm-alpine
wp_site_url:    "http://{{ ansible_facts['default_ipv4']['address'] }}"
wp_site_title:  "My WordPress Site"
wp_admin_user:  admin
wp_admin_email: "admin@example.com"

# ── MariaDB ───────────────────────────────────
db_image:       mariadb:10.11
db_name:        wordpress_db
db_user:        wordpress_user
db_port:        3306

# ── Redis ─────────────────────────────────────
redis_image:    redis:7.2-alpine
redis_port:     6379
redis_maxmem:   "128mb"

# ── Nginx ─────────────────────────────────────
nginx_image:    nginx:1.25.3-alpine
nginx_port:     80

# ── Resource Limits ───────────────────────────
nginx_memory:   "64m"
nginx_cpus:     "0.25"
wp_memory:      "256m"
wp_cpus:        "0.5"
db_memory:      "512m"
db_cpus:        "0.5"
redis_memory:   "128m"
redis_cpus:     "0.25"
EOF

templates/docker-compose.yml.j2

cat > ~/ansible/roles/wordpress/templates/docker-compose.yml.j2 << 'EOF'
{# WordPress Full Stack — docker-compose.yml #}
{# Managed by Ansible — DO NOT EDIT MANUALLY! #}
{# Generated: {{ ansible_facts['date_time']['iso8601'] }} #}
{# Host: {{ inventory_hostname }} #}

version: "3.8"

services:

  # ══════════════════════════════════════════
  # NGINX — Reverse Proxy
  # ══════════════════════════════════════════
  nginx:
    image: {{ nginx_image }}
    container_name: {{ stack_name }}_nginx
    restart: unless-stopped
    ports:
      - "{{ nginx_port }}:80"
    volumes:
      - {{ stack_config_dir }}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - wp_files:/var/www/html:ro
      - {{ stack_logs_dir }}/nginx:/var/log/nginx
      - /etc/localtime:/etc/localtime:ro
    networks:
      - frontend
    depends_on:
      wordpress:
        condition: service_started
    deploy:
      resources:
        limits:
          memory: {{ nginx_memory }}
          cpus:   "{{ nginx_cpus }}"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test:         ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost/"]
      interval:     30s
      timeout:      10s
      retries:      3
      start_period: 15s
    labels:
      stack:     {{ stack_name }}
      component: nginx
      env:       {{ app_env }}

  # ══════════════════════════════════════════
  # WORDPRESS — PHP-FPM
  # ══════════════════════════════════════════
  wordpress:
    image: {{ wp_image }}
    container_name: {{ stack_name }}_wordpress
    restart: unless-stopped
    env_file:
      - {{ stack_config_dir }}/wp.env
    volumes:
      - wp_files:/var/www/html
      - {{ stack_config_dir }}/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini:ro
      - /etc/localtime:/etc/localtime:ro
    networks:
      - frontend
      - backend
    depends_on:
      mariadb:
        condition: service_healthy
      redis:
        condition: service_healthy
    deploy:
      resources:
        limits:
          memory: {{ wp_memory }}
          cpus:   "{{ wp_cpus }}"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test:         ["CMD-SHELL", "php-fpm -t || exit 1"]
      interval:     30s
      timeout:      10s
      retries:      3
      start_period: 30s
    labels:
      stack:     {{ stack_name }}
      component: wordpress
      version:   "{{ stack_version }}"

  # ══════════════════════════════════════════
  # MARIADB — Database
  # ══════════════════════════════════════════
  mariadb:
    image: {{ db_image }}
    container_name: {{ stack_name }}_mariadb
    restart: unless-stopped
    environment:
      MYSQL_DATABASE:      {{ db_name }}
      MYSQL_USER:          {{ db_user }}
      MYSQL_PASSWORD:      {{ vault_db_password }}
      MYSQL_ROOT_PASSWORD: {{ vault_db_root_password }}
      MYSQL_COLLATION:     utf8mb4_unicode_ci
      MYSQL_CHARACTER_SET: utf8mb4
    volumes:
      - db_data:/var/lib/mysql
      - {{ stack_config_dir }}/mysql/custom.cnf:/etc/mysql/conf.d/custom.cnf:ro
      - /etc/localtime:/etc/localtime:ro
    networks:
      - backend
    deploy:
      resources:
        limits:
          memory: {{ db_memory }}
          cpus:   "{{ db_cpus }}"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test:         ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval:     10s
      timeout:      5s
      retries:      5
      start_period: 60s
    labels:
      stack:     {{ stack_name }}
      component: mariadb

  # ══════════════════════════════════════════
  # REDIS — Cache
  # ══════════════════════════════════════════
  redis:
    image: {{ redis_image }}
    container_name: {{ stack_name }}_redis
    restart: unless-stopped
    command: >
      redis-server
      --maxmemory {{ redis_maxmem }}
      --maxmemory-policy allkeys-lru
      --appendonly yes
      --requirepass {{ vault_redis_password }}
    volumes:
      - redis_data:/data
      - /etc/localtime:/etc/localtime:ro
    networks:
      - backend
    deploy:
      resources:
        limits:
          memory: {{ redis_memory }}
          cpus:   "{{ redis_cpus }}"
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test:         ["CMD", "redis-cli", "--no-auth-warning", "-a", "{{ vault_redis_password }}", "ping"]
      interval:     10s
      timeout:      5s
      retries:      5
      start_period: 10s
    labels:
      stack:     {{ stack_name }}
      component: redis

# ══════════════════════════════════════════
# VOLUMES
# ══════════════════════════════════════════
volumes:
  wp_files:
    name: {{ stack_name }}_wp_files
    labels:
      stack: {{ stack_name }}
      env:   {{ app_env }}

  db_data:
    name: {{ stack_name }}_db_data
    labels:
      stack: {{ stack_name }}
      env:   {{ app_env }}

  redis_data:
    name: {{ stack_name }}_redis_data
    labels:
      stack: {{ stack_name }}
      env:   {{ app_env }}

# ══════════════════════════════════════════
# NETWORKS
# ══════════════════════════════════════════
networks:
  frontend:
    name: {{ stack_name }}_frontend
    driver: bridge

  backend:
    name: {{ stack_name }}_backend
    driver: bridge
    internal: true
EOF

templates/wp.env.j2

cat > ~/ansible/roles/wordpress/templates/wp.env.j2 << 'EOF'
{# WordPress Environment Variables #}
{# Managed by Ansible Vault — DO NOT EDIT! #}

# ── Database ──────────────────────────────────
WORDPRESS_DB_HOST=mariadb:{{ db_port }}
WORDPRESS_DB_NAME={{ db_name }}
WORDPRESS_DB_USER={{ db_user }}
WORDPRESS_DB_PASSWORD={{ vault_db_password }}
WORDPRESS_TABLE_PREFIX=wp_

# ── WordPress Auth Keys (από Vault) ───────────
WORDPRESS_AUTH_KEY={{ vault_wp_auth_key }}
WORDPRESS_SECURE_AUTH_KEY={{ vault_wp_secure_auth_key }}
WORDPRESS_LOGGED_IN_KEY={{ vault_wp_logged_in_key }}
WORDPRESS_NONCE_KEY={{ vault_wp_nonce_key }}
WORDPRESS_AUTH_SALT={{ vault_wp_auth_salt }}
WORDPRESS_SECURE_AUTH_SALT={{ vault_wp_secure_auth_salt }}
WORDPRESS_LOGGED_IN_SALT={{ vault_wp_logged_in_salt }}
WORDPRESS_NONCE_SALT={{ vault_wp_nonce_salt }}

# ── WordPress Config ──────────────────────────
WORDPRESS_DEBUG={{ 'false' if app_env == 'production' else 'true' }}
WORDPRESS_CONFIG_EXTRA=
  define('WP_REDIS_HOST', 'redis');
  define('WP_REDIS_PORT', {{ redis_port }});
  define('WP_REDIS_PASSWORD', '{{ vault_redis_password }}');
  define('WP_REDIS_DATABASE', 0);
  define('WP_CACHE', true);
  define('WP_HOME', '{{ wp_site_url }}');
  define('WP_SITEURL', '{{ wp_site_url }}');
  define('WP_MEMORY_LIMIT', '256M');
EOF

templates/nginx.conf.j2

cat > ~/ansible/roles/wordpress/templates/nginx.conf.j2 << 'EOF'
{# Nginx config for WordPress #}
{# Managed by Ansible — DO NOT EDIT! #}

user nginx;
worker_processes {{ ansible_facts['processor_vcpus'] | default(1) }};
pid /var/run/nginx.pid;

events {
    worker_connections 1024;
    multi_accept on;
}

http {
    sendfile    on;
    tcp_nopush  on;
    tcp_nodelay on;
    keepalive_timeout 65;
    server_tokens off;

    include      /etc/nginx/mime.types;
    default_type application/octet-stream;

    # ── Logging ───────────────────────────────
    access_log /var/log/nginx/access.log;
    error_log  /var/log/nginx/error.log warn;

    # ── Gzip ──────────────────────────────────
    gzip            on;
    gzip_vary       on;
    gzip_proxied    any;
    gzip_comp_level 6;
    gzip_types
        text/plain text/css text/xml
        application/json application/javascript
        application/xml application/xml+rss
        text/javascript image/svg+xml;

    # ── WordPress Server ──────────────────────
    server {
        listen 80;
        server_name {{ inventory_hostname }} _;

        root  /var/www/html;
        index index.php index.html;

        client_max_body_size 64M;

        # ── Security headers ──────────────────
        add_header X-Frame-Options       "SAMEORIGIN"    always;
        add_header X-Content-Type-Options "nosniff"      always;
        add_header X-XSS-Protection      "1; mode=block" always;

        # ── WordPress permalinks ──────────────
        location / {
            try_files $uri $uri/ /index.php?$args;
        }

        # ── PHP via PHP-FPM ───────────────────
        location ~ \.php$ {
            fastcgi_pass   wordpress:9000;
            fastcgi_index  index.php;
            fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
            include        fastcgi_params;

            fastcgi_connect_timeout 60s;
            fastcgi_send_timeout    60s;
            fastcgi_read_timeout    60s;
            fastcgi_buffer_size     128k;
            fastcgi_buffers         4 256k;
        }

        # ── Static files ──────────────────────
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
            expires     1y;
            add_header  Cache-Control "public, immutable";
            log_not_found off;
            access_log  off;
        }

        # ── WordPress specific ────────────────
        location = /favicon.ico {
            log_not_found off;
            access_log    off;
        }

        location = /robots.txt {
            allow         all;
            log_not_found off;
            access_log    off;
        }

        # ── Block sensitive files ─────────────
        location ~* /\. {
            deny all;
        }

        location ~* /(?:uploads|files)/.*\.php$ {
            deny all;
        }

        # ── Block xmlrpc ──────────────────────
        location = /xmlrpc.php {
            deny all;
            access_log off;
            log_not_found off;
        }

        # ── wp-admin protection ───────────────
        location /wp-admin/ {
            location ~ \.php$ {
                fastcgi_pass  wordpress:9000;
                fastcgi_index index.php;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                include       fastcgi_params;
            }
        }
    }
}
EOF

tasks/main.yml

cat > ~/ansible/roles/wordpress/tasks/main.yml << 'EOF'
---
# ============================================================
# WordPress Stack — Main Entry Point
# ============================================================

- name: Setup directories and configs
  ansible.builtin.import_tasks:
    file: setup.yml
  tags: [setup, wordpress]

- name: Deploy WordPress stack
  ansible.builtin.import_tasks:
    file: deploy.yml
  tags: [deploy, wordpress]

- name: Verify deployment
  ansible.builtin.import_tasks:
    file: verify.yml
  tags: [verify, wordpress]
EOF

tasks/setup.yml

cat > ~/ansible/roles/wordpress/tasks/setup.yml << 'EOF'
---
# ============================================================
# Setup — Directories & Configuration Files
# ============================================================

# ── Directories ───────────────────────────────
- name: Create directory structure
  ansible.builtin.file:
    path:  "{{ item }}"
    state: directory
    mode:  '0755'
    owner: root
    group: docker
  loop:
    - "{{ stack_base_dir }}"
    - "{{ stack_config_dir }}"
    - "{{ stack_config_dir }}/nginx"
    - "{{ stack_config_dir }}/mysql"
    - "{{ stack_config_dir }}/php"
    - "{{ stack_data_dir }}"
    - "{{ stack_logs_dir }}"
    - "{{ stack_logs_dir }}/nginx"

# ── Docker Compose ────────────────────────────
- name: Render docker-compose.yml
  ansible.builtin.template:
    src:  docker-compose.yml.j2
    dest: "{{ stack_base_dir }}/docker-compose.yml"
    mode: '0644'
  register: compose_changed

# ── Environment file (secrets) ────────────────
- name: Render WordPress env file
  ansible.builtin.template:
    src:  wp.env.j2
    dest: "{{ stack_config_dir }}/wp.env"
    mode: '0600'
    owner: root
  no_log: true
  register: env_changed

# ── Nginx config ──────────────────────────────
- name: Render nginx configuration
  ansible.builtin.template:
    src:  nginx.conf.j2
    dest: "{{ stack_config_dir }}/nginx/nginx.conf"
    mode: '0644'
  register: nginx_changed

# ── PHP config ────────────────────────────────
- name: Deploy PHP uploads config
  ansible.builtin.copy:
    dest:  "{{ stack_config_dir }}/php/uploads.ini"
    mode:  '0644'
    content: |
      ; PHP Upload Configuration
      file_uploads = On
      upload_max_filesize = 64M
      post_max_size = 64M
      memory_limit = 256M
      max_execution_time = 300
      max_input_vars = 3000

# ── MariaDB config ────────────────────────────
- name: Deploy MariaDB configuration
  ansible.builtin.copy:
    dest:  "{{ stack_config_dir }}/mysql/custom.cnf"
    mode:  '0644'
    content: |
      [mysqld]
      character-set-server  = utf8mb4
      collation-server      = utf8mb4_unicode_ci
      innodb_buffer_pool_size = {{ (ansible_facts['memtotal_mb'] * 0.25) | int }}M
      innodb_log_file_size  = 32M
      max_connections       = 100
      slow_query_log        = 1
      slow_query_log_file   = /var/lib/mysql/slow-query.log
      long_query_time       = 2

      [client]
      default-character-set = utf8mb4

# ── Validate compose file ─────────────────────
- name: Validate docker-compose.yml
  ansible.builtin.command:
    cmd:   docker compose -f {{ stack_base_dir }}/docker-compose.yml config --quiet
    chdir: "{{ stack_base_dir }}"
  changed_when: false

- name: Setup complete
  ansible.builtin.debug:
    msg:
      - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      - "✅ Setup complete"
      - "Compose  : {{ 'Changed' if compose_changed.changed else 'OK' }}"
      - "Env file : {{ 'Changed' if env_changed.changed else 'OK' }}"
      - "Nginx    : {{ 'Changed' if nginx_changed.changed else 'OK' }}"
      - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
EOF

tasks/deploy.yml

cat > ~/ansible/roles/wordpress/tasks/deploy.yml << 'EOF'
---
# ============================================================
# Deploy — Docker Compose Stack
# ============================================================

# ── Pull images ───────────────────────────────
- name: Pull all Docker images
  ansible.builtin.command:
    cmd:   docker compose -f {{ stack_base_dir }}/docker-compose.yml pull
    chdir: "{{ stack_base_dir }}"
  register:     pull_result
  changed_when: "'Pull complete' in pull_result.stdout or 'Downloaded' in pull_result.stdout"

# ── Deploy stack ──────────────────────────────
- name: Deploy WordPress stack
  community.docker.docker_compose_v2:
    project_src:  "{{ stack_base_dir }}"
    project_name: "{{ stack_name }}"
    state:        present
    pull:         missing
  register: stack_result
  no_log: true

- name: Stack deployment result
  ansible.builtin.debug:
    msg: "Stack {{ stack_name }}: {{ 'Changed ✅' if stack_result.changed else 'Already up-to-date ✅' }}"

# ── Wait for MariaDB ──────────────────────────
- name: Wait for MariaDB to be healthy
  community.docker.docker_container_info:
    name: "{{ stack_name }}_mariadb"
  register: mariadb_info
  until: >
    mariadb_info.container is defined and
    mariadb_info.container.State.Health.Status == 'healthy'
  retries: 18
  delay:   10

- name: MariaDB status
  ansible.builtin.debug:
    msg: "✅ MariaDB: {{ mariadb_info.container.State.Health.Status }}"

# ── Wait for Redis ────────────────────────────
- name: Wait for Redis to be healthy
  community.docker.docker_container_info:
    name: "{{ stack_name }}_redis"
  register: redis_info
  until: >
    redis_info.container is defined and
    redis_info.container.State.Health.Status == 'healthy'
  retries: 6
  delay:   5

- name: Redis status
  ansible.builtin.debug:
    msg: "✅ Redis: {{ redis_info.container.State.Health.Status }}"

# ── Wait for WordPress ────────────────────────
- name: Wait for WordPress to start
  community.docker.docker_container_info:
    name: "{{ stack_name }}_wordpress"
  register: wp_info
  until: >
    wp_info.container is defined and
    wp_info.container.State.Running == true
  retries: 12
  delay:   5

- name: WordPress status
  ansible.builtin.debug:
    msg: "✅ WordPress: {{ wp_info.container.State.Status }}"

# ── Wait for Nginx ────────────────────────────
- name: Wait for Nginx to be healthy
  community.docker.docker_container_info:
    name: "{{ stack_name }}_nginx"
  register: nginx_info
  until: >
    nginx_info.container is defined and
    nginx_info.container.State.Running == true
  retries: 6
  delay:   5

- name: Nginx status
  ansible.builtin.debug:
    msg: "✅ Nginx: {{ nginx_info.container.State.Status }}"
EOF

tasks/verify.yml

cat > ~/ansible/roles/wordpress/tasks/verify.yml << 'EOF'
---
# ============================================================
# Verify — Επαλήθευση Deployment
# ============================================================

# ── All containers status ─────────────────────
- name: Get all stack containers
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ stack_base_dir }}/docker-compose.yml
      -p {{ stack_name }}
      ps --format "table {{.Name}}\t{{.Status}}\t{{.Health}}"
  register:     stack_ps
  changed_when: false

- name: Stack containers
  ansible.builtin.debug:
    msg: "{{ stack_ps.stdout_lines }}"

# ── HTTP check ────────────────────────────────
- name: HTTP health check
  ansible.builtin.uri:
    url:         "http://localhost:{{ nginx_port }}"
    status_code: [200, 301, 302]
    timeout:     30
  register:      http_check
  retries:       5
  delay:         10
  ignore_errors: true

# ── WordPress check ───────────────────────────
- name: WordPress wp-login.php check
  ansible.builtin.uri:
    url:         "http://localhost:{{ nginx_port }}/wp-login.php"
    status_code: 200
    timeout:     30
  register:      wp_check
  retries:       3
  delay:         5
  ignore_errors: true

# ── Database check ────────────────────────────
- name: Check MariaDB connectivity
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ stack_base_dir }}/docker-compose.yml
      -p {{ stack_name }}
      exec -T mariadb
      mysqladmin ping
      -u root
      -p{{ vault_db_root_password }}
  register:     db_check
  changed_when: false
  no_log: true
  ignore_errors: true

# ── Redis check ───────────────────────────────
- name: Check Redis connectivity
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ stack_base_dir }}/docker-compose.yml
      -p {{ stack_name }}
      exec -T redis
      redis-cli --no-auth-warning
      -a {{ vault_redis_password }}
      ping
  register:     redis_check
  changed_when: false
  no_log: true
  ignore_errors: true

# ── Resource usage ────────────────────────────
- name: Get container resource usage
  ansible.builtin.command:
    cmd: >
      docker stats --no-stream
      --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}"
      {{ stack_name }}_nginx
      {{ stack_name }}_wordpress
      {{ stack_name }}_mariadb
      {{ stack_name }}_redis
  register:     stats
  changed_when: false
  ignore_errors: true

# ── Disk usage ────────────────────────────────
- name: Check Docker disk usage
  ansible.builtin.command:
    cmd: docker system df
  register:     disk_usage
  changed_when: false

# ── Final report ──────────────────────────────
- name: Deployment Verification Report
  ansible.builtin.debug:
    msg:
      - "╔══════════════════════════════════════════╗"
      - "║   WORDPRESS STACK VERIFICATION           ║"
      - "╠══════════════════════════════════════════╣"
      - "║ Host : {{ inventory_hostname }}"
      - "║ URL  : {{ wp_site_url }}"
      - "╠══════════════════════════════════════════╣"
      - "║ SERVICES:                                ║"
      - "║ Nginx    : {{ '✅ Running' if nginx_info.container.State.Running else '❌ Stopped' }}"
      - "║ WordPress: {{ '✅ Running' if wp_info.container.State.Running    else '❌ Stopped' }}"
      - "║ MariaDB  : {{ '✅ Healthy' if mariadb_info.container.State.Health.Status == 'healthy' else '❌ Unhealthy' }}"
      - "║ Redis    : {{ '✅ Healthy' if redis_info.container.State.Health.Status  == 'healthy' else '❌ Unhealthy' }}"
      - "╠══════════════════════════════════════════╣"
      - "║ CHECKS:                                  ║"
      - "║ HTTP     : {{ '✅ OK' if not http_check.failed  else '❌ Failed' }}"
      - "║ WP Login : {{ '✅ OK' if not wp_check.failed    else '❌ Failed' }}"
      - "║ Database : {{ '✅ OK' if not db_check.failed    else '❌ Failed' }}"
      - "║ Redis    : {{ '✅ OK' if not redis_check.failed  else '❌ Failed' }}"
      - "╠══════════════════════════════════════════╣"
      - "║ RESOURCES:                               ║"
      - "{{ stats.stdout_lines | default(['N/A']) | join('\n') }}"
      - "╚══════════════════════════════════════════╝"
EOF

handlers/main.yml

cat > ~/ansible/roles/wordpress/handlers/main.yml << 'EOF'
---
- name: Reload nginx
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ stack_base_dir }}/docker-compose.yml
      -p {{ stack_name }}
      exec -T nginx nginx -s reload
  ignore_errors: "{{ ansible_check_mode }}"

- name: Restart wordpress
  ansible.builtin.command:
    cmd: >
      docker compose
      -f {{ stack_base_dir }}/docker-compose.yml
      -p {{ stack_name }}
      restart wordpress
  ignore_errors: "{{ ansible_check_mode }}"
EOF

Vault Variables

# Δημιουργία vault secrets
cat > /tmp/wp_vault.yml << 'EOF'
---
# WordPress Secrets
vault_db_password:      "WPDbPass@2026!"
vault_db_root_password: "WPRootPass@2026!"
vault_redis_password:   "WPRedis@2026!"

# WordPress Security Keys (πρέπει να είναι τυχαία!)
vault_wp_auth_key:          "put-random-string-here-1"
vault_wp_secure_auth_key:   "put-random-string-here-2"
vault_wp_logged_in_key:     "put-random-string-here-3"
vault_wp_nonce_key:         "put-random-string-here-4"
vault_wp_auth_salt:         "put-random-string-here-5"
vault_wp_secure_auth_salt:  "put-random-string-here-6"
vault_wp_logged_in_salt:    "put-random-string-here-7"
vault_wp_nonce_salt:        "put-random-string-here-8"
EOF

cp /tmp/wp_vault.yml \
   ~/ansible/inventory/group_vars/all/vault.yml

ansible-vault encrypt \
    ~/ansible/inventory/group_vars/all/vault.yml

rm /tmp/wp_vault.yml

Deployment Playbook

cat > ~/ansible/playbooks/wordpress-stack.yml << 'EOF'
---
# ============================================================
# WordPress Full Stack Deployment
# ============================================================

- name: Deploy WordPress Stack
  hosts: "{{ target | default('all_managed') }}"
  become: true
  gather_facts: true

  pre_tasks:

    - name: Pre-deployment info
      ansible.builtin.debug:
        msg:
          - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
          - "WordPress Stack Deployment"
          - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
          - "Host  : {{ inventory_hostname }}"
          - "IP    : {{ ansible_facts['default_ipv4']['address'] }}"
          - "OS    : {{ ansible_facts['distribution'] }} {{ ansible_facts['distribution_version'] }}"
          - "RAM   : {{ (ansible_facts['memtotal_mb'] / 1024) | round(1) }}GB"
          - "CPUs  : {{ ansible_facts['processor_vcpus'] }}"
          - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      tags: always

    - name: Verify Docker
      ansible.builtin.service_facts:

    - name: Assert Docker running
      ansible.builtin.assert:
        that:
          - ansible_facts.services['docker.service'].state == 'running'
        fail_msg: "❌ Docker is not running!"
      tags: always

  roles:
    - role: wordpress
      vars:
        wp_site_url:   "http://{{ ansible_facts['default_ipv4']['address'] }}"
        wp_site_title: "My WordPress Site"
        wp_admin_user: admin
        app_env:       production
      tags: wordpress

  post_tasks:

    - name: WordPress Access Info
      ansible.builtin.debug:
        msg:
          - "╔══════════════════════════════════════════╗"
          - "║   WORDPRESS DEPLOYMENT COMPLETE! 🎉      ║"
          - "╠══════════════════════════════════════════╣"
          - "║ Site URL   : http://{{ ansible_facts['default_ipv4']['address'] }}"
          - "║ Admin URL  : http://{{ ansible_facts['default_ipv4']['address'] }}/wp-admin/"
          - "║ Admin User : {{ wp_admin_user | default('admin') }}"
          - "╠══════════════════════════════════════════╣"
          - "║ Stack Commands:                          ║"
          - "║ Status: docker compose -p wordpress ps  ║"
          - "║ Logs  : docker compose -p wordpress logs║"
          - "║ Stop  : docker compose -p wordpress stop║"
          - "╚══════════════════════════════════════════╝"
      tags: always
EOF

Εκτέλεση

# ── Syntax check ──────────────────────────────
ansible-playbook playbooks/wordpress-stack.yml \
    --syntax-check

# ── Dry run ───────────────────────────────────
ansible-playbook playbooks/wordpress-stack.yml \
    --check \
    --limit nextcloud

# ── Full deployment ───────────────────────────
ansible-playbook playbooks/wordpress-stack.yml \
    --limit nextcloud \
    -v

# ── Μόνο setup ────────────────────────────────
ansible-playbook playbooks/wordpress-stack.yml \
    --tags setup \
    --limit nextcloud

# ── Μόνο deploy ───────────────────────────────
ansible-playbook playbooks/wordpress-stack.yml \
    --tags deploy \
    --limit nextcloud

# ── Μόνο verify ───────────────────────────────
ansible-playbook playbooks/wordpress-stack.yml \
    --tags verify \
    --limit nextcloud

Χρήσιμες Ad-hoc εντολές

# ── Stack status ──────────────────────────────
ansible nextcloud -m ansible.builtin.command \
    -a "docker compose -p wordpress ps" \
    --become

# ── Logs WordPress ────────────────────────────
ansible nextcloud -m ansible.builtin.command \
    -a "docker logs wordpress_wordpress --tail 30" \
    --become

# ── Logs MariaDB ──────────────────────────────
ansible nextcloud -m ansible.builtin.command \
    -a "docker logs wordpress_mariadb --tail 20" \
    --become

# ── Resource usage ────────────────────────────
ansible nextcloud -m ansible.builtin.command \
    -a "docker stats --no-stream" \
    --become

# ── Stop stack ────────────────────────────────
ansible nextcloud -m community.docker.docker_compose_v2 \
    -a "project_src=/opt/wordpress state=stopped" \
    --become

# ── Remove stack ──────────────────────────────
ansible nextcloud -m community.docker.docker_compose_v2 \
    -a "project_src=/opt/wordpress state=absent remove_volumes=true" \
    --become