D7.6 Πρακτικό παράδειγμα — Multi-container με Network & Volumes 🚀

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

Μια πλήρης multi-container εφαρμογή με proper networking και volume management:

┌─────────────────────────────────────────────────────────┐
│  Production Stack                                        │
│                                                         │
│  Internet :80/:443                                      │
│       │                                                 │
│       ▼                                                 │
│  ┌─────────────────────────────────────────────────┐   │
│  │  frontend_net                                   │   │
│  │  ┌──────────┐   ┌──────────────┐               │   │
│  │  │  nginx   │   │    myapp     │               │   │
│  │  │ (proxy)  │──►│  (node/py)   │               │   │
│  │  └──────────┘   └──────┬───────┘               │   │
│  └───────────────────────┼───────────────────────-┘   │
│                           │                             │
│  ┌────────────────────────┼────────────────────────┐   │
│  │  backend_net           │                        │   │
│  │                        ▼                        │   │
│  │  ┌──────────┐   ┌──────────────┐               │   │
│  │  │ postgres │   │    redis     │               │   │
│  │  │  :5432   │   │    :6379     │               │   │
│  │  └──────────┘   └──────────────┘               │   │
│  └─────────────────────────────────────────────────┘   │
│                                                         │
│  Volumes:                                               │
│  postgres_data → /var/lib/postgresql/data               │
│  redis_data    → /data                                  │
│  app_uploads   → /app/uploads                           │
│  nginx_certs   → /etc/nginx/certs (bind mount)          │
└─────────────────────────────────────────────────────────┘

Inventory

# hosts.ini
[all_managed]
nextcloud  ansible_host=192.168.1.10

[all_managed:vars]
ansible_user=ansible
ansible_ssh_private_key_file=~/.ssh/ansible_ed25519
ansible_become=true

Group Variables

# group_vars/all_managed.yml
app_name:    myapp
app_version: "{{ new_version | default('latest') }}"
app_env:     production
app_domain:  myapp.example.com

# Database
postgres_user: appuser
postgres_db:   appdb

# Resource limits
nginx_memory: "128m"
app_memory:   "512m"
db_memory:    "1g"
redis_memory: "256m"
# group_vars/vault.yml (encrypted with Ansible Vault)
vault_postgres_pass: "StrongP@ssw0rd!"
vault_redis_pass:    "RedisP@ssw0rd!"
vault_app_secret:    "SuperSecretKey!"

Το Playbook

---
# docker-fullstack-networking.yml
# Multi-container stack με Networks & Volumes

- name: Docker Fullstack — Networks & Volumes
  hosts: "{{ target | default('all_managed') }}"
  become: true
  gather_facts: true

  vars:
    # ── Stack config ───────────────────────────
    stack_name:  "{{ app_name }}_{{ app_env }}"
    cert_dir:    /etc/nginx/certs
    config_dir:  /opt/{{ app_name }}/config
    backup_dir:  /opt/backups/docker

    # ── Images ────────────────────────────────
    images:
      nginx:    "nginx:1.25-alpine"
      app:      "{{ app_name }}:{{ app_version }}"
      postgres: "postgres:15-alpine"
      redis:    "redis:7.2-alpine"

  # ════════════════════════════════════════════
  # PHASE 1: PREREQUISITES
  # ════════════════════════════════════════════
  tasks:

    - name: "════ PHASE 1: Prerequisites ════"
      ansible.builtin.debug:
        msg: "Έλεγχος και προετοιμασία περιβάλλοντος..."

    # ── Directories ───────────────────────────
    - name: "📁 Δημιουργία directories"
      ansible.builtin.file:
        path:  "{{ item.path }}"
        state: directory
        owner: "{{ item.owner | default('root') }}"
        group: "{{ item.group | default('root') }}"
        mode:  "{{ item.mode | default('0755') }}"
      loop:
        - { path: "{{ cert_dir }}",   mode: '0700' }
        - { path: "{{ config_dir }}", mode: '0755' }
        - { path: "{{ backup_dir }}/{{ inventory_hostname }}" }

    # ── Nginx config ──────────────────────────
    - name: "⚙️ Nginx config"
      ansible.builtin.template:
        src:   templates/nginx-proxy.conf.j2
        dest:  "{{ config_dir }}/nginx.conf"
        mode:  '0644'

    # ── Pull images ───────────────────────────
    - name: "📦 Pull Docker images"
      community.docker.docker_image:
        name:   "{{ item.key }}"
        tag:    "{{ item.value | split(':') | last }}"
        source: pull
      loop:     "{{ images | dict2items }}"
      loop_control:
        label: "{{ item.value }}"
      async:  300
      poll:   10

  # ════════════════════════════════════════════
  # PHASE 2: NETWORKS
  # ════════════════════════════════════════════

    - name: "════ PHASE 2: Networks ════"
      ansible.builtin.debug:
        msg: "Δημιουργία Docker networks..."

    - name: "🌐 Frontend network"
      community.docker.docker_network:
        name:   "{{ stack_name }}_frontend"
        state:  present
        driver: bridge
        ipam_config:
          - subnet:  "10.200.1.0/24"
            gateway: "10.200.1.1"
        labels:
          stack:       "{{ stack_name }}"
          layer:       frontend
          managed-by:  ansible

    - name: "🌐 Backend network (internal)"
      community.docker.docker_network:
        name:     "{{ stack_name }}_backend"
        state:    present
        driver:   bridge
        internal: true    # ← ΔΕΝ έχει internet access!
        ipam_config:
          - subnet:  "10.200.2.0/24"
            gateway: "10.200.2.1"
        labels:
          stack:      "{{ stack_name }}"
          layer:      backend
          managed-by: ansible

  # ════════════════════════════════════════════
  # PHASE 3: VOLUMES
  # ════════════════════════════════════════════

    - name: "════ PHASE 3: Volumes ════"
      ansible.builtin.debug:
        msg: "Δημιουργία Docker volumes..."

    - name: "💾 Δημιουργία named volumes"
      community.docker.docker_volume:
        name:  "{{ item.name }}"
        state: present
        labels:
          stack:       "{{ stack_name }}"
          purpose:     "{{ item.purpose }}"
          backup:      "{{ item.backup | string }}"
          managed-by:  ansible
      loop:
        - { name: "{{ stack_name }}_postgres", purpose: database, backup: true  }
        - { name: "{{ stack_name }}_redis",    purpose: cache,    backup: false }
        - { name: "{{ stack_name }}_uploads",  purpose: media,    backup: true  }
      loop_control:
        label: "{{ item.name }}"

  # ════════════════════════════════════════════
  # PHASE 4: CONTAINERS
  # ════════════════════════════════════════════

    - name: "════ PHASE 4: Containers ════"
      ansible.builtin.debug:
        msg: "Deployment containers..."

    # ── PostgreSQL ────────────────────────────
    - name: "🐘 PostgreSQL container"
      community.docker.docker_container:
        name:           "{{ stack_name }}_postgres"
        image:          "{{ images.postgres }}"
        state:          started
        restart_policy: unless-stopped
        memory:         "{{ db_memory }}"
        networks:
          - name: "{{ stack_name }}_backend"    # ← backend only!
        volumes:
          - "{{ stack_name }}_postgres:/var/lib/postgresql/data:rw"
        env:
          POSTGRES_DB:       "{{ postgres_db }}"
          POSTGRES_USER:     "{{ postgres_user }}"
          POSTGRES_PASSWORD: "{{ vault_postgres_pass }}"
          POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C"
        no_log: true
        healthcheck:
          test:         ["CMD-SHELL", "pg_isready -U {{ postgres_user }}"]
          interval:     10s
          timeout:      5s
          retries:      5
          start_period: 30s
        labels:
          stack:      "{{ stack_name }}"
          service:    postgres
          managed-by: ansible

    # ── Redis ─────────────────────────────────
    - name: "📮 Redis container"
      community.docker.docker_container:
        name:           "{{ stack_name }}_redis"
        image:          "{{ images.redis }}"
        state:          started
        restart_policy: unless-stopped
        memory:         "{{ redis_memory }}"
        networks:
          - name: "{{ stack_name }}_backend"    # ← backend only!
        volumes:
          - "{{ stack_name }}_redis:/data:rw"
        command: >
          redis-server
          --requirepass {{ vault_redis_pass }}
          --maxmemory {{ redis_memory }}
          --maxmemory-policy allkeys-lru
          --save 60 1000
        no_log: true
        healthcheck:
          test:     ["CMD", "redis-cli", "ping"]
          interval: 10s
          timeout:  5s
          retries:  3
        labels:
          stack:      "{{ stack_name }}"
          service:    redis
          managed-by: ansible

    # ── Αναμονή για backends ──────────────────
    - name: " Αναμονή για PostgreSQL"
      community.docker.docker_container_info:
        name: "{{ stack_name }}_postgres"
      register: pg_info
      until: >
        pg_info.container.State.Health is defined and
        pg_info.container.State.Health.Status == 'healthy'
      retries: 12
      delay:   5

    # ── Application ───────────────────────────
    - name: "🚀 Application container"
      community.docker.docker_container:
        name:           "{{ stack_name }}_app"
        image:          "{{ images.app }}"
        state:          started
        restart_policy: unless-stopped
        memory:         "{{ app_memory }}"
        networks:
          - name: "{{ stack_name }}_frontend"   # ← frontend
          - name: "{{ stack_name }}_backend"    # ← backend (για db/redis)
        volumes:
          - "{{ stack_name }}_uploads:/app/uploads:rw"
          - "{{ config_dir }}/app.env:/app/.env:ro"
        env:
          NODE_ENV:     "{{ app_env }}"
          DATABASE_URL: >
            postgresql://{{ postgres_user }}:{{ vault_postgres_pass }}
            @{{ stack_name }}_postgres:5432/{{ postgres_db }}
          REDIS_URL:    "redis://:{{ vault_redis_pass }}@{{ stack_name }}_redis:6379"
          SECRET_KEY:   "{{ vault_app_secret }}"
          APP_VERSION:  "{{ app_version }}"
        no_log: true
        healthcheck:
          test:         ["CMD-SHELL", "wget -qO- http://localhost:3000/health || exit 1"]
          interval:     15s
          timeout:      10s
          retries:      5
          start_period: 30s
        labels:
          stack:      "{{ stack_name }}"
          service:    app
          version:    "{{ app_version }}"
          managed-by: ansible

    # ── Nginx Reverse Proxy ───────────────────
    - name: "🌐 Nginx container"
      community.docker.docker_container:
        name:           "{{ stack_name }}_nginx"
        image:          "{{ images.nginx }}"
        state:          started
        restart_policy: unless-stopped
        memory:         "{{ nginx_memory }}"
        ports:
          - "80:80"
          - "443:443"
        networks:
          - name: "{{ stack_name }}_frontend"   # ← frontend only!
        volumes:
          - "{{ config_dir }}/nginx.conf:/etc/nginx/nginx.conf:ro"
          - "{{ cert_dir }}:/etc/nginx/certs:ro"
          - "{{ stack_name }}_uploads:/app/uploads:ro"    # ← static files
        healthcheck:
          test:     ["CMD-SHELL", "wget -qO- http://localhost/ || exit 1"]
          interval: 15s
          timeout:  5s
          retries:  3
        labels:
          stack:      "{{ stack_name }}"
          service:    nginx
          managed-by: ansible

  # ════════════════════════════════════════════
  # PHASE 5: VERIFICATION
  # ════════════════════════════════════════════

    - name: "════ PHASE 5: Verification ════"
      ansible.builtin.debug:
        msg: "Επαλήθευση stack..."

    - name: "🔍 Έλεγχος όλων των containers"
      community.docker.docker_container_info:
        name: "{{ item }}"
      register: stack_info
      loop:
        - "{{ stack_name }}_postgres"
        - "{{ stack_name }}_redis"
        - "{{ stack_name }}_app"
        - "{{ stack_name }}_nginx"
      loop_control:
        label: "{{ item }}"

    - name: "🔍 HTTP endpoint check"
      ansible.builtin.uri:
        url:         "http://localhost/health"
        status_code: 200
      register:  http_check
      retries:   5
      delay:     5
      until:     http_check.status == 200

    - name: "📊 Stack Report"
      ansible.builtin.debug:
        msg:
          - "╔══════════════════════════════════════════════╗"
          - "   DOCKER STACK DEPLOYED!                   ║"
          - "╠══════════════════════════════════════════════╣"
          - "  Stack  : {{ stack_name }}"
          - "  Version: {{ app_version }}"
          - "╠══════════════════════════════════════════════╣"
          - "  Networks:"
          - "    frontend: {{ stack_name }}_frontend"
          - "    backend:  {{ stack_name }}_backend (internal)"
          - "╠══════════════════════════════════════════════╣"
          - "  Volumes:"
          - "    postgres: {{ stack_name }}_postgres"
          - "    redis:    {{ stack_name }}_redis"
          - "    uploads:  {{ stack_name }}_uploads"
          - "╠══════════════════════════════════════════════╣"
          - "  Containers:"
          - "{% for item in stack_info.results %}\
                 {{ item.item | replace(stack_name + '_', '') }}: {{ item.container.State.Status }}\
             {% endfor %}"
          - "╠══════════════════════════════════════════════╣"
          - "  HTTP: {{ '✅ OK' if not http_check.failed else '❌ FAILED' }}"
          - "  URL:  http://{{ ansible_facts['default_ipv4']['address'] }}"
          - "╚══════════════════════════════════════════════╝"

Εκτέλεση

# Deploy
ansible-playbook playbooks/docker-fullstack-networking.yml \
    --limit nextcloud \
    -e "new_version=2.5.0" \
    --ask-vault-pass

# Dry run
ansible-playbook playbooks/docker-fullstack-networking.yml \
    --limit nextcloud \
    -e "new_version=2.5.0" \
    --check --diff

# Verify stack
ansible nextcloud -m ansible.builtin.command \
    -a "docker ps --filter label=stack=myapp_production" \
    --become

# Network inspection
ansible nextcloud -m ansible.builtin.command \
    -a "docker network inspect myapp_production_backend" \
    --become

Σύνοψη D7.6

Τι χρησιμοποιήσαμε:
│
├── Networks:
│   ├── frontend_net → nginx + app
│   ├── backend_net  → app + postgres + redis
│   └── internal: true → backend ΔΕΝ έχει internet
│
├── Volumes:
│   ├── Named: postgres, redis, uploads
│   └── Bind: config, certs (read-only)
│
├── DNS:
│   └── Containers "μιλάνε" με ονόματα:
│       DATABASE_URL → @postgres_container:5432
│       REDIS_URL    → @redis_container:6379
│
├── Security:
│   ├── Secrets: no_log, vault
│   ├── Read-only volumes: :ro
│   ├── Internal networks: no internet for DBs
│   └── Minimal port exposure
│
└── Order:
    directories → networks → volumes → containers