D3.3 Πρακτικό Παράδειγμα — Multi-container Application 🚀
Τι θα φτιάξουμε
┌─────────────────────────────────────────────────────────┐
│ Multi-container Application Stack │
│ │
│ Internet │
│ │ :80 │
│ ▼ │
│ ┌─────────┐ :3000 ┌─────────┐ │
│ │ nginx │────────────►│ myapp │ │
│ │ (proxy) │ │ (node) │ │
│ └─────────┘ └────┬────┘ │
│ │ :5432 │
│ ┌────▼────┐ │
│ │postgres │ │
│ │ (db) │ │
│ └────┬────┘ │
│ │ │
│ ┌────▼────┐ │
│ │ redis │ │
│ │ (cache) │ │
│ └─────────┘ │
│ │
│ Networks: │
│ ├── frontend: nginx ↔ myapp │
│ └── backend: myapp ↔ postgres ↔ redis │
└─────────────────────────────────────────────────────────┘
Δομή αρχείων
# Δημιουργία δομής
mkdir -p ~/ansible/roles/myapp/{tasks,templates,defaults,handlers,files}
mkdir -p ~/ansible/roles/myapp/files/html
tree ~/ansible/roles/myapp/
# roles/myapp/
# ├── defaults/
# │ └── main.yml
# ├── files/
# │ └── html/
# │ └── index.html
# ├── handlers/
# │ └── main.yml
# ├── tasks/
# │ ├── main.yml
# │ ├── networks.yml
# │ ├── volumes.yml
# │ ├── postgres.yml
# │ ├── redis.yml
# │ ├── app.yml
# │ └── nginx.yml
# └── templates/
# ├── nginx.conf.j2
# └── app.env.j2
defaults/main.yml
cat > ~/ansible/roles/myapp/defaults/main.yml << 'EOF'
---
# ============================================================
# MyApp Role Defaults
# ============================================================
# ── Application ───────────────────────────────
app_name: myapp
app_version: "1.0.0"
app_env: production
app_port: 3000
app_replicas: 1
# ── Nginx ─────────────────────────────────────
nginx_image: nginx:1.25.3-alpine
nginx_host_port: 80
nginx_ssl_port: 443
# ── PostgreSQL ────────────────────────────────
postgres_image: postgres:15.4-alpine
postgres_db: "{{ app_name }}_db"
postgres_user: "{{ app_name }}_user"
postgres_port: 5432
# ── Redis ─────────────────────────────────────
redis_image: redis:7.2-alpine
redis_port: 6379
redis_maxmem: "128mb"
# ── Networks ──────────────────────────────────
frontend_network: "{{ app_name }}_frontend"
backend_network: "{{ app_name }}_backend"
# ── Volumes ───────────────────────────────────
postgres_volume: "{{ app_name }}_postgres_data"
redis_volume: "{{ app_name }}_redis_data"
app_volume: "{{ app_name }}_app_data"
# ── Resource Limits ───────────────────────────
nginx_memory: "64m"
nginx_cpus: "0.25"
app_memory: "256m"
app_cpus: "0.5"
postgres_memory: "256m"
postgres_cpus: "0.5"
redis_memory: "128m"
redis_cpus: "0.25"
# ── Paths ─────────────────────────────────────
app_base_dir: "/opt/{{ app_name }}"
app_config_dir: "/opt/{{ app_name }}/config"
app_data_dir: "/opt/{{ app_name }}/data"
app_logs_dir: "/opt/{{ app_name }}/logs"
EOF
tasks/main.yml
cat > ~/ansible/roles/myapp/tasks/main.yml << 'EOF'
---
# ============================================================
# MyApp Role — Main Entry Point
# ============================================================
- name: Create app directories
ansible.builtin.file:
path: "{{ item }}"
state: directory
mode: '0755'
owner: root
group: docker
loop:
- "{{ app_base_dir }}"
- "{{ app_config_dir }}"
- "{{ app_data_dir }}"
- "{{ app_logs_dir }}"
tags: [setup, directories]
- name: Setup Docker networks
ansible.builtin.import_tasks:
file: networks.yml
tags: [setup, networks]
- name: Setup Docker volumes
ansible.builtin.import_tasks:
file: volumes.yml
tags: [setup, volumes]
- name: Deploy PostgreSQL
ansible.builtin.import_tasks:
file: postgres.yml
tags: [deploy, postgres, database]
- name: Deploy Redis
ansible.builtin.import_tasks:
file: redis.yml
tags: [deploy, redis, cache]
- name: Deploy Application
ansible.builtin.import_tasks:
file: app.yml
tags: [deploy, app]
- name: Deploy Nginx
ansible.builtin.import_tasks:
file: nginx.yml
tags: [deploy, nginx, proxy]
EOF
tasks/networks.yml
cat > ~/ansible/roles/myapp/tasks/networks.yml << 'EOF'
---
# ============================================================
# Docker Networks Setup
# ============================================================
- name: Create frontend network
community.docker.docker_network:
name: "{{ frontend_network }}"
state: present
driver: bridge
ipam_config:
- subnet: "172.20.0.0/24"
- name: Create backend network
community.docker.docker_network:
name: "{{ backend_network }}"
state: present
driver: bridge
internal: true # ← δεν έχει πρόσβαση στο internet!
ipam_config:
- subnet: "172.21.0.0/24"
- name: Verify networks
ansible.builtin.command:
cmd: "docker network ls --filter name={{ app_name }}"
register: network_list
changed_when: false
- name: Network status
ansible.builtin.debug:
msg: "{{ network_list.stdout_lines }}"
EOF
tasks/volumes.yml
cat > ~/ansible/roles/myapp/tasks/volumes.yml << 'EOF'
---
# ============================================================
# Docker Volumes Setup
# ============================================================
- name: Create PostgreSQL volume
community.docker.docker_volume:
name: "{{ postgres_volume }}"
state: present
labels:
app: "{{ app_name }}"
component: postgres
env: "{{ app_env }}"
- name: Create Redis volume
community.docker.docker_volume:
name: "{{ redis_volume }}"
state: present
labels:
app: "{{ app_name }}"
component: redis
- name: Create App volume
community.docker.docker_volume:
name: "{{ app_volume }}"
state: present
labels:
app: "{{ app_name }}"
component: app
- name: Verify volumes
ansible.builtin.command:
cmd: "docker volume ls --filter name={{ app_name }}"
register: volume_list
changed_when: false
- name: Volume status
ansible.builtin.debug:
msg: "{{ volume_list.stdout_lines }}"
EOF
tasks/postgres.yml
cat > ~/ansible/roles/myapp/tasks/postgres.yml << 'EOF'
---
# ============================================================
# PostgreSQL Container
# ============================================================
- name: Pull PostgreSQL image
community.docker.docker_image:
name: postgres
tag: 15.4-alpine
source: pull
- name: Deploy PostgreSQL container
community.docker.docker_container:
name: "{{ app_name }}_postgres"
image: "{{ postgres_image }}"
state: started
restart_policy: unless-stopped
env:
POSTGRES_DB: "{{ postgres_db }}"
POSTGRES_USER: "{{ postgres_user }}"
POSTGRES_PASSWORD: "{{ vault_postgres_password }}"
PGDATA: /var/lib/postgresql/data/pgdata
no_log: true
volumes:
- "{{ postgres_volume }}:/var/lib/postgresql/data"
- /etc/localtime:/etc/localtime:ro
networks:
- name: "{{ backend_network }}"
aliases:
- postgres
- db # ← alias για εύκολη σύνδεση
memory: "{{ postgres_memory }}"
cpus: "{{ postgres_cpus }}"
labels:
app: "{{ app_name }}"
component: postgres
env: "{{ app_env }}"
log_driver: json-file
log_options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U {{ postgres_user }} -d {{ postgres_db }}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
- name: Wait for PostgreSQL to be healthy
community.docker.docker_container_info:
name: "{{ app_name }}_postgres"
register: pg_info
until: >
pg_info.container.State.Health.Status == 'healthy'
retries: 12
delay: 5
- name: PostgreSQL status
ansible.builtin.debug:
msg: "✅ PostgreSQL: {{ pg_info.container.State.Health.Status }}"
EOF
tasks/redis.yml
cat > ~/ansible/roles/myapp/tasks/redis.yml << 'EOF'
---
# ============================================================
# Redis Container
# ============================================================
- name: Pull Redis image
community.docker.docker_image:
name: redis
tag: 7.2-alpine
source: pull
- name: Deploy Redis container
community.docker.docker_container:
name: "{{ app_name }}_redis"
image: "{{ redis_image }}"
state: started
restart_policy: unless-stopped
command: >
redis-server
--maxmemory {{ redis_maxmem }}
--maxmemory-policy allkeys-lru
--appendonly yes
--requirepass {{ vault_redis_password }}
volumes:
- "{{ redis_volume }}:/data"
- /etc/localtime:/etc/localtime:ro
networks:
- name: "{{ backend_network }}"
aliases:
- redis
- cache # ← alias
memory: "{{ redis_memory }}"
cpus: "{{ redis_cpus }}"
labels:
app: "{{ app_name }}"
component: redis
log_driver: json-file
log_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
no_log: true
- name: Wait for Redis to be healthy
community.docker.docker_container_info:
name: "{{ app_name }}_redis"
register: redis_info
until: >
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 }}"
EOF
tasks/app.yml
cat > ~/ansible/roles/myapp/tasks/app.yml << 'EOF'
---
# ============================================================
# Application Container
# (Προσομοιώνουμε με nginx που επιστρέφει JSON)
# ============================================================
- name: Pull app image
community.docker.docker_image:
name: nginx
tag: 1.25.3-alpine
source: pull
- name: Deploy app environment file
ansible.builtin.template:
src: app.env.j2
dest: "{{ app_config_dir }}/.env"
mode: '0600'
owner: root
no_log: true
- name: Deploy application container
community.docker.docker_container:
name: "{{ app_name }}_app"
image: "nginx:1.25.3-alpine"
state: started
restart_policy: unless-stopped
env_file:
- "{{ app_config_dir }}/.env"
volumes:
- "{{ app_volume }}:/app/data"
- "{{ app_config_dir }}/.env:/app/.env:ro"
- /etc/localtime:/etc/localtime:ro
networks:
- name: "{{ frontend_network }}"
aliases:
- app
- "{{ app_name }}"
- name: "{{ backend_network }}"
memory: "{{ app_memory }}"
cpus: "{{ app_cpus }}"
labels:
app: "{{ app_name }}"
component: app
version: "{{ app_version }}"
env: "{{ app_env }}"
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: 20s
no_log: true
- name: Wait for app to be healthy
community.docker.docker_container_info:
name: "{{ app_name }}_app"
register: app_info
until: >
app_info.container.State.Health.Status in ['healthy', 'starting']
retries: 12
delay: 5
- name: App status
ansible.builtin.debug:
msg: "✅ App: {{ app_info.container.State.Status }}"
EOF
tasks/nginx.yml
cat > ~/ansible/roles/myapp/tasks/nginx.yml << 'EOF'
---
# ============================================================
# Nginx Reverse Proxy Container
# ============================================================
- name: Create nginx config directory
ansible.builtin.file:
path: "{{ app_config_dir }}/nginx"
state: directory
mode: '0755'
- name: Deploy nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: "{{ app_config_dir }}/nginx/nginx.conf"
mode: '0644'
notify: Reload nginx container
- name: Pull nginx image
community.docker.docker_image:
name: nginx
tag: 1.25.3-alpine
source: pull
- name: Deploy nginx reverse proxy container
community.docker.docker_container:
name: "{{ app_name }}_nginx"
image: "{{ nginx_image }}"
state: started
restart_policy: unless-stopped
ports:
- "{{ nginx_host_port }}:80"
volumes:
- "{{ app_config_dir }}/nginx/nginx.conf:/etc/nginx/nginx.conf:ro"
- "{{ app_logs_dir }}:/var/log/nginx"
- /etc/localtime:/etc/localtime:ro
networks:
- name: "{{ frontend_network }}"
memory: "{{ nginx_memory }}"
cpus: "{{ nginx_cpus }}"
labels:
app: "{{ app_name }}"
component: nginx-proxy
env: "{{ app_env }}"
log_driver: json-file
log_options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
- name: Wait for nginx to be healthy
community.docker.docker_container_info:
name: "{{ app_name }}_nginx"
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
- name: Nginx status
ansible.builtin.debug:
msg: "✅ Nginx proxy: {{ nginx_info.container.State.Status }}"
EOF
templates/nginx.conf.j2
cat > ~/ansible/roles/myapp/templates/nginx.conf.j2 << 'EOF'
{# Nginx Reverse Proxy Configuration #}
{# 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_types text/plain text/css application/json
application/javascript text/xml;
# ── Rate limiting ─────────────────────────
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
# ── Upstream ──────────────────────────────
upstream {{ app_name }}_backend {
least_conn;
server app:{{ app_port }};
# ← 'app' = container name/alias στο frontend network
keepalive 32;
}
# ── Health check endpoint ─────────────────
server {
listen 80;
server_name {{ inventory_hostname }} _;
# Health check
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# ── Static files ──────────────────────
location /static/ {
alias /var/www/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# ── API endpoints ─────────────────────
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://{{ app_name }}_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# ── Application ───────────────────────
location / {
proxy_pass http://{{ app_name }}_backend;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# ── 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;
}
}
}
EOF
templates/app.env.j2
cat > ~/ansible/roles/myapp/templates/app.env.j2 << 'EOF'
# Application Environment
# Managed by Ansible — DO NOT EDIT!
NODE_ENV={{ app_env }}
APP_VERSION={{ app_version }}
PORT={{ app_port }}
# Database
DB_HOST=db
DB_PORT={{ postgres_port }}
DB_NAME={{ postgres_db }}
DB_USER={{ postgres_user }}
DB_PASSWORD={{ vault_postgres_password }}
DATABASE_URL=postgresql://{{ postgres_user }}:{{ vault_postgres_password }}@db:{{ postgres_port }}/{{ postgres_db }}
# Redis
REDIS_HOST=cache
REDIS_PORT={{ redis_port }}
REDIS_PASSWORD={{ vault_redis_password }}
REDIS_URL=redis://:{{ vault_redis_password }}@cache:{{ redis_port }}
# App
SECRET_KEY={{ vault_app_secret_key }}
EOF
handlers/main.yml
cat > ~/ansible/roles/myapp/handlers/main.yml << 'EOF'
---
- name: Reload nginx container
community.docker.docker_container:
name: "{{ app_name }}_nginx"
state: started
restart: true
ignore_errors: "{{ ansible_check_mode }}"
- name: Restart app container
community.docker.docker_container:
name: "{{ app_name }}_app"
state: started
restart: true
ignore_errors: "{{ ansible_check_mode }}"
EOF
Vault Variables
# Δημιουργία vault secrets
cat > /tmp/app_vault.yml << 'EOF'
---
vault_postgres_password: "PgPass@2026#Secure!"
vault_redis_password: "RedisPass@2026!"
vault_app_secret_key: "AppSecret@2026#SuperLong!"
EOF
cp /tmp/app_vault.yml \
~/ansible/inventory/group_vars/all/vault.yml
ansible-vault encrypt \
~/ansible/inventory/group_vars/all/vault.yml
rm /tmp/app_vault.yml
Deployment Playbook
cat > ~/ansible/playbooks/deploy-multicontainer.yml << 'EOF'
---
# ============================================================
# Multi-container Application Deployment
# ============================================================
- name: Deploy Multi-container Application
hosts: "{{ target | default('all_managed') }}"
become: true
gather_facts: true
pre_tasks:
- name: Pre-deployment info
ansible.builtin.debug:
msg:
- "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- "Multi-container Deployment"
- "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
- "Host : {{ inventory_hostname }}"
- "App : {{ app_name }} v{{ app_version }}"
- "Env : {{ app_env }}"
- "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
tags: always
- name: Verify Docker is running
ansible.builtin.service_facts:
- name: Assert Docker is running
ansible.builtin.assert:
that:
- ansible_facts.services['docker.service'].state == 'running'
fail_msg: "❌ Docker is not running!"
tags: always
roles:
- role: myapp
vars:
app_name: mywebapp
app_version: "1.0.0"
app_env: production
post_tasks:
- name: Verify all containers running
ansible.builtin.command:
cmd: "docker ps --filter name=mywebapp --format '{{ '{{' }}.Names{{ '}}' }}\t{{ '{{' }}.Status{{ '}}' }}'"
register: containers_status
changed_when: false
tags: verify
- name: Show container status
ansible.builtin.debug:
msg: "{{ containers_status.stdout_lines }}"
tags: verify
- name: Verify HTTP response
ansible.builtin.uri:
url: "http://localhost:80/health"
status_code: 200
register: http_check
retries: 5
delay: 5
ignore_errors: true
tags: verify
- name: Deployment Report
ansible.builtin.debug:
msg:
- "╔══════════════════════════════════════╗"
- "║ DEPLOYMENT COMPLETE! 🎉 ║"
- "╠══════════════════════════════════════╣"
- "║ App : mywebapp v1.0.0 ║"
- "║ Host : {{ inventory_hostname }}"
- "║ URL : http://{{ ansible_facts['default_ipv4']['address'] }}"
- "║ HTTP : {{ '✅ OK' if not http_check.failed else '❌ Check manually' }}"
- "╠══════════════════════════════════════╣"
- "║ Containers: ║"
- "{{ containers_status.stdout_lines | join('\n') }}"
- "╚══════════════════════════════════════╝"
tags: always
EOF
Εκτέλεση
# ── Syntax check ──────────────────────────────
ansible-playbook playbooks/deploy-multicontainer.yml \
--syntax-check
# ── List tasks ────────────────────────────────
ansible-playbook playbooks/deploy-multicontainer.yml \
--list-tasks
# ── Dry run ───────────────────────────────────
ansible-playbook playbooks/deploy-multicontainer.yml \
--check \
--limit nextcloud
# ── Full deployment ───────────────────────────
ansible-playbook playbooks/deploy-multicontainer.yml \
--limit nextcloud \
-v
# ── Μόνο database ─────────────────────────────
ansible-playbook playbooks/deploy-multicontainer.yml \
--tags postgres \
--limit nextcloud
# ── Update μόνο app ───────────────────────────
ansible-playbook playbooks/deploy-multicontainer.yml \
--tags app \
--limit nextcloud \
-e "app_version=1.1.0"
# ── Verify deployment ─────────────────────────
ansible-playbook playbooks/deploy-multicontainer.yml \
--tags verify \
--limit nextcloud
Χρήσιμες Ad-hoc εντολές
# ── Status όλων των containers ────────────────
ansible nextcloud -m ansible.builtin.command \
-a "docker ps --filter name=mywebapp" \
--become
# ── Logs από postgres ─────────────────────────
ansible nextcloud -m ansible.builtin.command \
-a "docker logs mywebapp_postgres --tail 20" \
--become
# ── Logs από app ──────────────────────────────
ansible nextcloud -m ansible.builtin.command \
-a "docker logs mywebapp_app --tail 20" \
--become
# ── Resource usage ────────────────────────────
ansible nextcloud -m ansible.builtin.command \
-a "docker stats --no-stream" \
--become
# ── Network inspect ───────────────────────────
ansible nextcloud -m ansible.builtin.command \
-a "docker network inspect mywebapp_backend" \
--become
# ── Exec σε container ────────────────────────
ansible nextcloud -m ansible.builtin.command \
-a "docker exec mywebapp_postgres pg_isready -U mywebapp_user" \
--become
# ── HTTP test ─────────────────────────────────
ansible nextcloud -m ansible.builtin.uri \
-a "url=http://localhost/health status_code=200"