D11.6 Πρακτικό παράδειγμα — Rootless Production Stack 🚀

Αρχιτεκτονική

Host: Rocky Linux 9 (RHEL-family)
User: appuser (UID 1000)

Rootless containers (ως appuser):
├── nginx    → port 8080 (NAT από 80 με firewalld)
├── myapp    → internal network
└── postgres → internal network, named volume

systemd (user scope):
├── nginx.service    ← quadlet
├── myapp.service    ← quadlet
└── postgres.service ← quadlet

Auto-start: loginctl enable-linger appuser

Full playbook

---
# rootless-stack.yml

- name: Rootless Podman Stack
  hosts: "{{ target | default('nextcloud') }}"
  become: true
  gather_facts: true

  vars:
    podman_user:    appuser
    podman_uid:     1000
    app_version:    "{{ new_version | mandatory }}"
    app_domain:     "example.com"
    quadlet_dir:    "/home/{{ podman_user }}/.config/containers/systemd"
    secrets_dir:    "/home/{{ podman_user }}/.secrets"

  tasks:

    # ════════════════════════════════════════
    # PHASE 1: PREREQUISITES
    # ════════════════════════════════════════
    - name: "📦 Install Podman stack"
      ansible.builtin.package:
        name: [podman, podman-compose, buildah, slirp4netns, fuse-overlayfs]
        state: present

    - name: "👤 Create appuser"
      ansible.builtin.user:
        name:    "{{ podman_user }}"
        uid:     "{{ podman_uid }}"
        shell:   /bin/bash
        state:   present

    - name: "🔑 Linger (auto-start on boot)"
      ansible.builtin.command:
        cmd: "loginctl enable-linger {{ podman_user }}"
      changed_when: true

    - name: "📁 Directories"
      ansible.builtin.file:
        path:  "{{ item }}"
        state: directory
        owner: "{{ podman_user }}"
        mode:  '0700'
      loop:
        - "{{ quadlet_dir }}"
        - "{{ secrets_dir }}"

    # ════════════════════════════════════════
    # PHASE 2: SECRETS (files, not env vars)
    # ════════════════════════════════════════
    - name: "🔐 Write secrets"
      ansible.builtin.copy:
        content: "{{ item.value }}"
        dest:    "{{ secrets_dir }}/{{ item.key }}"
        owner:   "{{ podman_user }}"
        mode:    '0600'
      loop:
        - { key: pg_password,  value: "{{ vault_postgres_pass }}" }
        - { key: app_secret,   value: "{{ vault_app_secret }}" }
      loop_control:
        label: "{{ item.key }}"
      no_log: true

    # ════════════════════════════════════════
    # PHASE 3: QUADLETS
    # ════════════════════════════════════════
    - name: "⚙️ PostgreSQL quadlet"
      ansible.builtin.copy:
        dest:  "{{ quadlet_dir }}/postgres.container"
        owner: "{{ podman_user }}"
        mode:  '0644'
        content: |
          [Unit]
          Description=PostgreSQL Database
          After=network-online.target

          [Container]
          Image=postgres:15-alpine
          Network=myapp.network
          Volume=postgres_data.volume:/var/lib/postgresql/data:Z
          Environment=POSTGRES_PASSWORD_FILE=/run/secrets/pg_password
          Environment=POSTGRES_USER=appuser
          Environment=POSTGRES_DB=mydb
          Secret={{ secrets_dir }}/pg_password,type=bind,target=/run/secrets/pg_password
          ReadOnly=true
          Tmpfs=/tmp:size=64m
          Tmpfs=/var/run/postgresql:size=16m

          [Service]
          Restart=always

          [Install]
          WantedBy=default.target
      notify: Reload user systemd

    - name: "⚙️ App quadlet"
      ansible.builtin.template:
        src:   templates/myapp.container.j2
        dest:  "{{ quadlet_dir }}/myapp.container"
        owner: "{{ podman_user }}"
        mode:  '0644'
      notify: Reload user systemd

    - name: "⚙️ Nginx quadlet"
      ansible.builtin.copy:
        dest:  "{{ quadlet_dir }}/nginx.container"
        owner: "{{ podman_user }}"
        mode:  '0644'
        content: |
          [Unit]
          Description=Nginx Reverse Proxy
          After=myapp.service

          [Container]
          Image=nginx:1.25-alpine
          Network=myapp.network
          PublishPort=8080:80
          Volume=/home/{{ podman_user }}/nginx.conf:/etc/nginx/nginx.conf:ro,Z
          ReadOnly=true
          Tmpfs=/tmp:size=32m
          Tmpfs=/var/cache/nginx:size=128m
          Tmpfs=/var/run:size=16m

          [Service]
          Restart=always

          [Install]
          WantedBy=default.target
      notify: Reload user systemd

    - name: "⚙️ Network quadlet"
      ansible.builtin.copy:
        dest:  "{{ quadlet_dir }}/myapp.network"
        owner: "{{ podman_user }}"
        mode:  '0644'
        content: |
          [Network]
          Internal=false
          DNS=true
      notify: Reload user systemd

    - name: "⚙️ Volume quadlet"
      ansible.builtin.copy:
        dest:  "{{ quadlet_dir }}/postgres_data.volume"
        owner: "{{ podman_user }}"
        mode:  '0644'
        content: |
          [Volume]
          Label=app=myapp
      notify: Reload user systemd

    # ════════════════════════════════════════
    # PHASE 4: FIREWALL (NAT 80 → 8080)
    # ════════════════════════════════════════
    - name: "🔥 Firewalld: Allow HTTP"
      ansible.posix.firewalld:
        port:      "80/tcp"
        permanent: true
        state:     enabled
      notify: Reload firewalld

    - name: "🔥 Firewalld: Port forward 80→8080"
      ansible.posix.firewalld:
        rich_rule: >
          rule family="ipv4"
          forward-port port="80" protocol="tcp" to-port="8080"
        permanent: true
        state:     enabled
      notify: Reload firewalld

    # ════════════════════════════════════════
    # PHASE 5: START SERVICES
    # ════════════════════════════════════════
    - name: "▶️ Start quadlet services"
      ansible.builtin.systemd:
        name:    "{{ item }}"
        state:   started
        enabled: true
        scope:   user
      loop:
        - postgres
        - myapp
        - nginx
      become:      true
      become_user: "{{ podman_user }}"
      environment:
        XDG_RUNTIME_DIR: "/run/user/{{ podman_uid }}"

    # ════════════════════════════════════════
    # PHASE 6: VERIFY
    # ════════════════════════════════════════
    - name: "🔍 HTTP check"
      ansible.builtin.uri:
        url:         "http://localhost:8080"
        status_code: 200
      retries: 6
      delay:   5
      until:   not http_check.failed
      register: http_check

    - name: "📊 Stack Report"
      ansible.builtin.debug:
        msg:
          - "╔══════════════════════════════════════╗"
          - "  🦭 ROOTLESS STACK DEPLOYED!         ║"
          - "  Host:  {{ inventory_hostname }}     ║"
          - "  User:  {{ podman_user }}            ║"
          - "  App:   v{{ app_version }}           ║"
          - "╠══════════════════════════════════════╣"
          - "   Rootless (no root daemon)         ║"
          - "   systemd Quadlets (auto-start)     ║"
          - "   SELinux labels (:Z)               ║"
          - "   Secrets as files (not env vars)   ║"
          - "╚══════════════════════════════════════╝"

  handlers:

    - name: Reload user systemd
      ansible.builtin.command:
        cmd: "systemctl --user daemon-reload"
      become:      true
      become_user: "{{ podman_user }}"
      environment:
        XDG_RUNTIME_DIR: "/run/user/{{ podman_uid }}"

    - name: Reload firewalld
      ansible.builtin.service:
        name:  firewalld
        state: reloaded
# Εκτέλεση
ansible-playbook rootless-stack.yml \
    -e "new_version=2.5.0" \
    --ask-vault-pass

# Verify
ssh nextcloud "systemctl --user status nginx myapp postgres"
ssh nextcloud "podman ps"

Σύνοψη Κεφ. D11

Podman με Ansible
│
├── D11.1: Rootless + daemonless (security benefits)
├── D11.2: Install + subuid/subgid + linger
├── D11.3: containers.podman modules (ίδιο API)
├── D11.4: Quadlets → systemd auto-start
├── D11.5: podman-compose + play kube
└── D11.6: Full rootless production stack
         User scope, SELinux, file secrets, firewalld NAT

Ideal για: RHEL/Rocky/Fedora + high-security