D3.2 Docker Containers με Ansible 🐳

Container Lifecycle

┌─────────────────────────────────────────────────────────┐
│  Container States                                       │
│                                                         │
│  absent ──► created ──► started ──► stopped             │
│     ▲                      │            │               │
│     └──────────────────────┴────────────┘               │
│                                                         │
│  Ansible states:                                        │
│  ├── started  → τρέχει (δημιουργεί αν δεν υπάρχει)      │
│  ├── stopped  → σταματημένο (δεν διαγράφει)             │
│  ├── absent   → διαγραμμένο                             │
│  └── present  → υπάρχει (δεν ξεκινά αναγκαστικά)        │
└─────────────────────────────────────────────────────────┘

Βασικές λειτουργίες

tasks:

  # ── Start container ───────────────────────
  - name: Start nginx
    community.docker.docker_container:
      name:  nginx_web
      image: nginx:1.25.3-alpine
      state: started

  # ── Stop container ────────────────────────
  - name: Stop nginx
    community.docker.docker_container:
      name:  nginx_web
      state: stopped

  # ── Restart container ─────────────────────
  - name: Restart nginx
    community.docker.docker_container:
      name:  nginx_web
      state: started
      restart: true              # ← force restart

  # ── Remove container ──────────────────────
  - name: Remove nginx
    community.docker.docker_container:
      name:       nginx_web
      state:      absent
      force_kill: true           # ← kill αν τρέχει

  # ── Recreate container ────────────────────
  - name: Recreate nginx
    community.docker.docker_container:
      name:            nginx_web
      image:           nginx:1.25.3-alpine
      state:           started
      recreate:        true      # ← πάντα recreate

Port Mapping

tasks:

  # ── Single port ───────────────────────────
  - name: Single port mapping
    community.docker.docker_container:
      name:  web
      image: nginx:alpine
      state: started
      ports:
        - "80:80"                # host:container

  # ── Multiple ports ────────────────────────
  - name: Multiple port mappings
    community.docker.docker_container:
      name:  web
      image: nginx:alpine
      state: started
      ports:
        - "80:80"
        - "443:443"

  # ── Custom host port ──────────────────────
  - name: Custom host port
    community.docker.docker_container:
      name:  web
      image: nginx:alpine
      state: started
      ports:
        - "8080:80"              # host 8080 → container 80

  # ── Bind to specific interface ────────────
  - name: Bind to localhost only
    community.docker.docker_container:
      name:  api
      image: myapi:latest
      state: started
      ports:
        - "127.0.0.1:3000:3000" # μόνο localhost

  # ── Variables για ports ───────────────────
  - name: Port από variables
    community.docker.docker_container:
      name:  "{{ app_name }}"
      image: "{{ app_image }}"
      state: started
      ports:
        - "{{ app_host_port }}:{{ app_container_port }}"

Environment Variables

tasks:

  # ── Simple env vars ───────────────────────
  - name: Container με env vars
    community.docker.docker_container:
      name:  myapp
      image: myapp:1.0
      state: started
      env:
        NODE_ENV:  production
        PORT:      "3000"
        LOG_LEVEL: info
      no_log: false            # ← OK αν δεν υπάρχουν secrets

  # ── Με secrets (πάντα no_log!) ────────────
  - name: Container με secrets
    community.docker.docker_container:
      name:  myapp
      image: myapp:1.0
      state: started
      env:
        NODE_ENV:     production
        DB_HOST:      postgres
        DB_NAME:      "{{ db_name }}"
        DB_USER:      "{{ db_user }}"
        DB_PASSWORD:  "{{ vault_db_password }}"
        SECRET_KEY:   "{{ vault_app_secret }}"
        API_TOKEN:    "{{ vault_api_token }}"
    no_log: true               # ← ΠΑΝΤΑ για secrets!

  # ── Env vars από αρχείο ───────────────────
  - name: Container με env_file
    community.docker.docker_container:
      name:    myapp
      image:   myapp:1.0
      state:   started
      env_file:
        - /opt/myapp/.env       # ← αρχείο με env vars

  # ── Δυναμικά env vars ─────────────────────
  - name: Dynamic env vars
    community.docker.docker_container:
      name:  myapp
      image: myapp:1.0
      state: started
      env:
        APP_VERSION:  "{{ app_version }}"
        BUILD_DATE:   "{{ ansible_facts['date_time']['date'] }}"
        HOSTNAME:     "{{ inventory_hostname }}"
        ENVIRONMENT:  "{{ app_env | default('production') }}"

Volumes

tasks:

  # ── Named volume ──────────────────────────
  - name: Container με named volume
    community.docker.docker_container:
      name:  postgres
      image: postgres:15-alpine
      state: started
      volumes:
        - postgres_data:/var/lib/postgresql/data

  # ── Bind mount ────────────────────────────
  - name: Container με bind mount
    community.docker.docker_container:
      name:  nginx
      image: nginx:alpine
      state: started
      volumes:
        - /var/www/html:/usr/share/nginx/html:ro    # ro = read-only!
        - /etc/nginx/nginx.conf:/etc/nginx/nginx.conf:ro

  # ── Mixed volumes ─────────────────────────
  - name: Container με mixed volumes
    community.docker.docker_container:
      name:  myapp
      image: myapp:1.0
      state: started
      volumes:
        - myapp_data:/app/data              # named volume
        - /opt/myapp/config:/app/config:ro  # bind mount (ro)
        - /etc/localtime:/etc/localtime:ro  # timezone

  # ── Tmpfs mount ───────────────────────────
  - name: Container με tmpfs
    community.docker.docker_container:
      name:  myapp
      image: myapp:1.0
      state: started
      mounts:
        - type:   tmpfs
          target: /tmp
          tmpfs_size: "100m"

Networks

tasks:

  # ── Default bridge network ────────────────
  - name: Container στο default network
    community.docker.docker_container:
      name:  myapp
      image: myapp:1.0
      state: started
      # ← χωρίς network → default bridge

  # ── Custom network ────────────────────────
  - name: Container σε custom network
    community.docker.docker_container:
      name:  myapp
      image: myapp:1.0
      state: started
      networks:
        - name: app_network

  # ── Πολλαπλά networks ─────────────────────
  - name: Container σε πολλά networks
    community.docker.docker_container:
      name:  nginx
      image: nginx:alpine
      state: started
      networks:
        - name: frontend         # ← public traffic
        - name: backend          # ← internal traffic

  # ── Network με aliases ────────────────────
  - name: Container με network alias
    community.docker.docker_container:
      name:  postgres
      image: postgres:15-alpine
      state: started
      networks:
        - name:    backend
          aliases:
            - db                 # ← άλλα containers μπορούν να χρησιμοποιούν "db"
            - database

  # ── Host network ──────────────────────────
  - name: Container με host network
    community.docker.docker_container:
      name:    myapp
      image:   myapp:1.0
      state:   started
      network_mode: host        # ← χρησιμοποιεί host networking

Resource Limits

tasks:

  # ── Memory limits ─────────────────────────
  - name: Container με memory limit
    community.docker.docker_container:
      name:   myapp
      image:  myapp:1.0
      state:  started
      memory: "512m"            # ← max 512MB RAM
      memory_swap: "1g"         # ← max 1GB swap

  # ── CPU limits ────────────────────────────
  - name: Container με CPU limit
    community.docker.docker_container:
      name:  myapp
      image: myapp:1.0
      state: started
      cpus:  "0.5"              # ← 0.5 CPU cores

  # ── CPU shares ────────────────────────────
  - name: Container με CPU shares
    community.docker.docker_container:
      name:       myapp
      image:      myapp:1.0
      state:      started
      cpu_shares: 512           # ← relative weight (default: 1024)

  # ── Full resource config ──────────────────
  - name: Production container με limits
    community.docker.docker_container:
      name:        myapp
      image:       "myapp:{{ app_version }}"
      state:       started
      memory:      "{{ container_memory | default('512m') }}"
      memory_swap: "{{ container_memory_swap | default('1g') }}"
      cpus:        "{{ container_cpus | default('1.0') }}"
      cpu_shares:  1024
      ulimits:
        - nofile:65536:65536    # ← max open files

Restart Policies

tasks:

  # ── no (default) ──────────────────────────
  - name: Container χωρίς restart
    community.docker.docker_container:
      name:           myapp
      image:          myapp:1.0
      state:          started
      restart_policy: "no"      # ← δεν ξεκινά αυτόματα

  # ── always ────────────────────────────────
  - name: Container με always restart
    community.docker.docker_container:
      name:           myapp
      image:          myapp:1.0
      state:          started
      restart_policy: always    # ← πάντα restart (ακόμα κι αν manual stop)

  # ── unless-stopped ────────────────────────
  - name: Container με unless-stopped
    community.docker.docker_container:
      name:           myapp
      image:          myapp:1.0
      state:          started
      restart_policy: unless-stopped   # ← restart εκτός αν manual stop ✅

  # ── on-failure ────────────────────────────
  - name: Container με on-failure
    community.docker.docker_container:
      name:                myapp
      image:               myapp:1.0
      state:               started
      restart_policy:      on-failure
      restart_retries:     3         # ← max 3 προσπάθειες

Health Checks

tasks:

  # ── HTTP health check ─────────────────────
  - name: Container με HTTP health check
    community.docker.docker_container:
      name:  myapp
      image: myapp:1.0
      state: started
      healthcheck:
        test:     ["CMD", "curl", "-f", "http://localhost:3000/health"]
        interval: 30s
        timeout:  10s
        retries:  3
        start_period: 40s       # ← περίμενε 40s πριν ξεκινήσεις checks

  # ── Shell health check ────────────────────
  - name: Container με shell health check
    community.docker.docker_container:
      name:  postgres
      image: postgres:15-alpine
      state: started
      healthcheck:
        test:     ["CMD-SHELL", "pg_isready -U postgres"]
        interval: 10s
        timeout:  5s
        retries:  5

  # ── Wait for healthy ──────────────────────
  - name: Περίμενε container να γίνει healthy
    community.docker.docker_container_info:
      name: myapp
    register: health_check
    until: >
      health_check.container.State.Health.Status == 'healthy'
    retries: 12
    delay:   5

  - name: Health status
    ansible.builtin.debug:
      msg: >
        myapp health:
        {{ health_check.container.State.Health.Status }}

Logging Configuration

tasks:

  # ── json-file (default) ───────────────────
  - name: Container με json-file logging
    community.docker.docker_container:
      name:       myapp
      image:      myapp:1.0
      state:      started
      log_driver: json-file
      log_options:
        max-size: "10m"
        max-file: "3"

  # ── syslog ────────────────────────────────
  - name: Container με syslog
    community.docker.docker_container:
      name:       myapp
      image:      myapp:1.0
      state:      started
      log_driver: syslog
      log_options:
        syslog-address: "tcp://localhost:514"
        tag:            "myapp"

  # ── journald ──────────────────────────────
  - name: Container με journald
    community.docker.docker_container:
      name:       myapp
      image:      myapp:1.0
      state:      started
      log_driver: journald
      log_options:
        tag: "myapp-{{ app_version }}"

Labels

tasks:

  - name: Container με labels
    community.docker.docker_container:
      name:  myapp
      image: "myapp:{{ app_version }}"
      state: started
      labels:
        # ── Application metadata ──────────────
        app:         myapp
        version:     "{{ app_version }}"
        environment: "{{ app_env }}"

        # ── Deployment metadata ───────────────
        deployed-by:   ansible
        deployed-on:   "{{ ansible_facts['date_time']['date'] }}"
        deployed-from: "{{ inventory_hostname }}"

        # ── Traefik labels (reverse proxy) ────
        traefik.enable:                      "true"
        traefik.http.routers.myapp.rule:     "Host(`myapp.example.com`)"
        traefik.http.services.myapp.loadbalancer.server.port: "3000"

Container Info & Monitoring

tasks:

  # ── Get container info ────────────────────
  - name: Get container information
    community.docker.docker_container_info:
      name: myapp
    register: app_info

  - name: Show container details
    ansible.builtin.debug:
      msg:
        - "Name   : {{ app_info.container.Name }}"
        - "Status : {{ app_info.container.State.Status }}"
        - "Running: {{ app_info.container.State.Running }}"
        - "IP     : {{ app_info.container.NetworkSettings.Networks | dict2items | map(attribute='value') | map(attribute='IPAddress') | first | default('N/A') }}"
        - "Image  : {{ app_info.container.Config.Image }}"
        - "Created: {{ app_info.container.Created[:10] }}"
    when: app_info.exists

  # ── Conditional restart ───────────────────
  - name: Restart αν unhealthy
    community.docker.docker_container:
      name:  myapp
      state: started
      restart: true
    when:
      - app_info.exists
      - app_info.container.State.Health is defined
      - app_info.container.State.Health.Status == 'unhealthy'

  # ── Docker stats ──────────────────────────
  - name: Get container stats
    ansible.builtin.command:
      cmd: >
        docker stats myapp
        --no-stream
        --format "{{ '{{' }}.CPUPerc{{ '}}' }}\t{{ '{{' }}.MemUsage{{ '}}' }}"
    register:     container_stats
    changed_when: false
    ignore_errors: true

  - name: Show stats
    ansible.builtin.debug:
      msg: "myapp stats: {{ container_stats.stdout }}"
    when: not container_stats.failed

Πλήρες παράδειγμα — Nginx Container

---
# nginx-container.yml

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

  vars:
    nginx_container_name: nginx_web
    nginx_image:          nginx:1.25.3-alpine
    nginx_host_port:      80
    nginx_web_root:       /var/www/html

  tasks:

    # ── Προετοιμασία ──────────────────────────
    - name: Create web directories
      ansible.builtin.file:
        path:  "{{ item }}"
        state: directory
        mode:  '0755'
      loop:
        - "{{ nginx_web_root }}"
        - /etc/nginx/conf.d

    - name: Deploy index.html
      ansible.builtin.copy:
        dest:  "{{ nginx_web_root }}/index.html"
        mode:  '0644'
        content: |
          <!DOCTYPE html>
          <html>
          <head><title>{{ inventory_hostname }}</title></head>
          <body>
            <h1>✅ {{ inventory_hostname }}</h1>
            <p>Running in Docker container</p>
            <p>Image: {{ nginx_image }}</p>
          </body>
          </html>

    # ── Network & Volume ──────────────────────
    - name: Create nginx network
      community.docker.docker_network:
        name:  nginx_net
        state: present

    # ── Pull image ────────────────────────────
    - name: Pull nginx image
      community.docker.docker_image:
        name:   nginx
        tag:    1.25.3-alpine
        source: pull

    # ── Deploy container ──────────────────────
    - name: Deploy nginx container
      community.docker.docker_container:
        name:            "{{ nginx_container_name }}"
        image:           "{{ nginx_image }}"
        state:           started
        restart_policy:  unless-stopped
        ports:
          - "{{ nginx_host_port }}:80"
        volumes:
          - "{{ nginx_web_root }}:/usr/share/nginx/html:ro"
          - /etc/localtime:/etc/localtime:ro
        networks:
          - name: nginx_net
        labels:
          app:     nginx
          managed: ansible
        memory:    "128m"
        cpus:      "0.25"
        log_driver: json-file
        log_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: 10s
      register: nginx_result

    # ── Wait for healthy ──────────────────────
    - name: Wait for nginx healthy
      community.docker.docker_container_info:
        name: "{{ nginx_container_name }}"
      register: nginx_info
      until: >
        nginx_info.container.State.Health is not defined or
        nginx_info.container.State.Health.Status in ['healthy', 'starting']
      retries: 6
      delay:   5

    # ── Verify ────────────────────────────────
    - name: Verify nginx responds
      ansible.builtin.uri:
        url:         "http://localhost:{{ nginx_host_port }}"
        status_code: 200
      register:      http_check
      retries:       3
      delay:         5
      ignore_errors: true

    # ── Report ────────────────────────────────
    - name: Deployment Report
      ansible.builtin.debug:
        msg:
          - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
          - " Nginx Container Deployed"
          - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
          - "Container: {{ nginx_container_name }}"
          - "Image    : {{ nginx_image }}"
          - "Port     : {{ nginx_host_port }}"
          - "Status   : {{ nginx_info.container.State.Status }}"
          - "HTTP     : {{ '✅ OK' if not http_check.failed else '❌ Failed' }}"
          - "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
      tags: always
# Εκτέλεση
ansible-playbook playbooks/nginx-container.yml \
    --limit nextcloud \
    -v

# Verify
ansible nextcloud -m ansible.builtin.uri \
    -a "url=http://localhost status_code=200"

Σύνοψη D3.2

Docker Containers με Ansible
├── States:
│   ├── started  → τρέχει ✅
│   ├── stopped  → σταματημένο
│   ├── absent   → διαγραμμένο
│   └── present  → υπάρχει
│
├── Configuration:
│   ├── ports    → host:container mapping
│   ├── env      → environment variables (no_log!)
│   ├── volumes  → named volumes + bind mounts
│   ├── networks → custom Docker networks
│   └── labels   → metadata
│
├── Resources:
│   ├── memory, memory_swap
│   ├── cpus, cpu_shares
│   └── ulimits
│
├── Reliability:
│   ├── restart_policy: unless-stopped
│   ├── healthcheck: test, interval, retries
│   └── Wait for healthy με until/retries
│
└── Monitoring:
    ├── docker_container_info
    ├── docker stats
    └── Conditional restart αν unhealthy