No description
Find a file
Sergey Rukavishnikov f823b18052 docs: link Russian README and admin guide translations
Add cross-links to Russian versions of the README and admin guide.
Update backlog and memory-bank notes to record the new localized
operator documentation.
2026-06-06 00:17:11 +05:00
.ansible/test-logs test(ansible): add VM validation logs and criteria updates 2026-05-25 02:19:12 +05:00
apps docs: document standalone and submodule usage modes 2026-06-05 23:32:28 +05:00
docs docs: link Russian README and admin guide translations 2026-06-06 00:17:11 +05:00
inventory test(ansible): add VM validation logs and criteria updates 2026-05-25 02:19:12 +05:00
memory-bank docs: link Russian README and admin guide translations 2026-06-06 00:17:11 +05:00
playbooks refactor: migrate to host_apps declarative app deployment 2026-05-24 04:12:07 +05:00
roles docs: clarify Podman network persistence and rendering 2026-05-25 22:43:53 +05:00
.clinerules modified: .clinerules 2026-05-24 03:53:09 +05:00
.gitignore Initial project setup: specification, backlog, test plan, memory-bank 2026-05-03 20:28:22 +05:00
ansible.cfg feat: implement core Ansible roles (tasks 01-04) 2026-05-04 00:07:14 +05:00
deploy_app.yml docs: document standalone and submodule usage modes 2026-06-05 23:32:28 +05:00
README.md docs: link Russian README and admin guide translations 2026-06-06 00:17:11 +05:00
README.ru.md docs: link Russian README and admin guide translations 2026-06-06 00:17:11 +05:00
site.yml docs: overhaul README to detail app descriptors and layout 2026-05-25 00:16:47 +05:00

Ansible Podman Kube Play (apkp)

Russian version: README.ru.md.

Ansible automation for deploying containerized applications on Debian 13 using Podman (rootful) with Podman Quadlets. Applications are described as Kubernetes Pod manifests and managed as systemd services via the Quadlet generator. The infrastructure uses an ipvlan network for direct LAN connectivity (no NAT, no port mapping).

Prerequisites

  • Target host: Debian 13 (bookworm)
  • Ansible: core ≥ 2.15 on the control node
  • SSH access: passwordless SSH + sudo to the target host
  • Network: a physical NIC available as the ipvlan parent interface

Usage Modes

APKP supports two repository layouts:

  • Standalone mode: this repository contains the framework, sample apps, inventory, and test/dev configuration. This is the default development and test workflow.
  • Vendor/submodule mode: a private ops repository contains real inventory/, real apps/, secrets, and runbooks, while APKP is pinned under vendor/apkp/ as a git submodule.

The public APKP repository should contain framework code, roles, generic playbooks, examples, tests, specifications, and documentation. A private ops repository should contain home-lab specific desired state.

Quick Start — Standalone Mode

# 1. Clone the repository
git clone <repo-url> apkp
cd apkp

# 2. Create your inventory
cp inventory/host_vars/example-host.yml inventory/host_vars/myhost.yml
# Edit myhost.yml — set the NIC name, IP ranges, and host_apps list

# 3. Add your host to inventory/hosts.yml

# 4. Run the full playbook (infrastructure + applications)
ansible-playbook site.yml

# Run only infrastructure setup
ansible-playbook site.yml --tags infra

# Run only application deployment
ansible-playbook site.yml --tags deploy

# Remove an application (data preserved by default)
ansible-playbook playbooks/remove-app.yml --limit <host> -e app=<app_name>

# Remove an application AND delete its data
ansible-playbook playbooks/remove-app.yml --limit <host> -e app=<app_name> -e remove_data=true

Quick Start — Vendor/Submodule Mode

In a private ops repository, pin APKP as a submodule and keep real inventory/apps next to it:

mkdir -p homelab-apkp
cd homelab-apkp
git init

git submodule add <apkp-repo-url> vendor/apkp

# Create private inventory/ and apps/ in this ops repository.
ansible-playbook vendor/apkp/site.yml \
  -i inventory/hosts.yml \
  -e apkp_apps_dir="$PWD/apps"

Application removal uses the playbook from the pinned APKP submodule:

ansible-playbook vendor/apkp/playbooks/remove-app.yml \
  -i inventory/hosts.yml \
  --limit <host> \
  -e app=<app_name>

Pin upgrades by checking out a reviewed APKP tag or commit inside vendor/apkp/, then committing the submodule pointer in the ops repository.

How It Works

Source of Truth

In standalone mode, this repository is the single source of truth for your lab. In vendor/submodule mode, the private ops repository is the source of truth and APKP is a pinned framework dependency.

  • What apps exist → directories under apps/.
  • What runs wherehost_apps lists in inventory/host_vars/<host>.yml.
  • What data exists → on the target hosts under /opt/apps/<name>/. Data is never deleted by Ansible unless remove_data=true is passed explicitly.

Every meaningful change should be a git commit.

Application Descriptor

Each application is described by a self-contained directory under apps/<name>/:

apps/sample-app/
├── app.yml           # metadata, data dirs, template list, app_vars
└── templates/
    └── pod.yaml.j2   # K8s manifest template(s)

The app.yml file defines:

app_name: sample-app
app_data_dirs:
  - data
app_manifests_templates:
  - templates/pod.yaml.j2
app_vars:
  image: busybox:stable
  port: 8080

app_manifests_templates entries are interpreted relative to the application directory (apps/<name>/) unless an absolute path is provided. The app catalog defaults to {{ playbook_dir }}/apps and can be overridden with apkp_apps_dir for external ops repositories. The descriptor filename defaults to app.yml and can be overridden with apkp_app_descriptor_filename.

Host Declaration

Each host declares which applications it runs via host_apps:

# inventory/host_vars/myhost.yml
podman_infra_network_interface: eth0
# ... network configuration ...

host_apps:
  - sample-app
  - sample-app-2

Repository Layout

apkp/
├── ansible.cfg
├── site.yml                    # thin orchestrator: infra + loop over host_apps
├── deploy_app.yml              # helper: load app descriptor + include deploy role
├── playbooks/
│   └── remove-app.yml          # removes one app: -e app=<name>
├── apps/                       # catalog of application descriptors
│   ├── sample-app/
│   │   ├── app.yml
│   │   └── templates/pod.yaml.j2
│   └── sample-app-2/
│       ├── app.yml
│       └── templates/pod.yaml.j2
├── inventory/
│   ├── hosts.yml
│   └── host_vars/
│       └── <host>.yml          # network vars + host_apps list
└── roles/
    ├── podman_infra/           # host preparation + network
    ├── podman_app_deploy/      # application deployment engine
    └── podman_app_remove/      # application teardown

Recommended private ops repository layout:

homelab-apkp/
├── ansible.cfg
├── Makefile
├── README.md
├── inventory/
│   ├── hosts.yml
│   ├── group_vars/
│   │   ├── all.yml
│   │   └── vault.yml
│   └── host_vars/
├── apps/
│   └── <app>/
│       ├── app.yml
│       └── templates/
│           └── pod.yaml.j2
├── runbooks/
└── vendor/
    └── apkp/                  # git submodule pinned to an APKP tag/commit

Inventory Configuration

inventory/hosts.yml

all:
  hosts:
    myhost:
      ansible_host: 192.168.1.10
      ansible_user: admin

inventory/host_vars/myhost.yml

# Network interface that will serve as the ipvlan parent.
# Must exist and be UP on the target host.
podman_infra_network_interface: eth0

# IPv4 configuration
podman_infra_network_ipv4_subnet: 192.168.100.0/24
podman_infra_network_ipv4_gateway: 192.168.100.1
podman_infra_network_ipv4_iprange: 192.168.100.128/25

# IPv6 configuration (dual-stack)
podman_infra_network_ipv6_subnet: fd00:abcd::/64
podman_infra_network_ipv6_gateway: fd00:abcd::1
podman_infra_network_ipv6_iprange: fd00:abcd::100-fd00:abcd::1ff

# Applications to deploy on this host
host_apps:
  - sample-app

See inventory/host_vars/example-host.yml for a fully annotated example.

Roles

podman_infra — Host Infrastructure

Installs Podman, enables the Podman API socket, and creates a persistent ipvlan network.

Variable Default Description
podman_infra_packages [podman, systemd-container] Packages to install
podman_infra_network_name ipvlan-platform Name of the podman network
podman_infra_network_interface Required. Parent NIC for ipvlan
podman_infra_network_ipv4_subnet Required. IPv4 subnet (CIDR)
podman_infra_network_ipv4_gateway Required. IPv4 gateway
podman_infra_network_ipv4_iprange Required. IPv4 pool (CIDR or start-end)
podman_infra_network_ipv6_subnet Required. IPv6 subnet (CIDR)
podman_infra_network_ipv6_gateway Required. IPv6 gateway
podman_infra_network_ipv6_iprange Required. IPv6 pool (CIDR or start-end)

podman_app_deploy — Application Deployment Engine

Creates an isolated system user, prepares bind-mount directories, generates a consolidated K8s YAML manifest, and registers a Quadlet .kube service. This role is called once per application via deploy_app.yml.

Variable Default Description
app_name Required. Application name (no spaces)
podman_app_deploy_base_dir /opt/apps Root directory for all apps
app_data_dirs [] Subdirectories for hostPath bind-mounts
app_manifests_templates Required. List of Jinja2 template paths, normalized by deploy_app.yml
app_vars {} Free-form dictionary passed to templates
podman_app_deploy_infra_network_name ipvlan-platform Network name (auto-resolved from podman_infra_network_name)
podman_app_deploy_auto_restart true Restart service on manifest change

podman_app_remove — Application Removal

Stops and removes the application service, Quadlet file, manifests, and system user. Data directories are preserved unless explicitly requested.

Variable Default Description
podman_app_remove_app_name Required. Application name
podman_app_remove_base_dir /opt/apps Root directory for all apps
podman_app_remove_remove_data false Also delete data directories

Adding a New Application

  1. Create apps/<name>/app.yml with application metadata:

    app_name: my-app
    app_data_dirs:
      - data
      - config
    app_manifests_templates:
      - templates/pod.yaml.j2
    app_vars:
      image: myimage:latest
      port: 8080
    
  2. Create the template apps/<name>/templates/pod.yaml.j2:

    • Use {{ app_uid }} / {{ app_gid }} in securityContext
    • Use {{ app_vars.* }} for customizable values
    • Use hostPath volumes pointing to {{ podman_app_deploy_base_dir }}/{{ app_name }}/<subdir>
    • All manifests for one app are concatenated into a single file — separate resources with ---
  3. Add the application to host_apps in the target host's host_vars/<host>.yml:

    host_apps:
      - sample-app
      - my-app
    
  4. Deploy:

    ansible-playbook site.yml --limit <host> --tags deploy
    

In vendor/submodule mode, run the equivalent command from the ops repository:

ansible-playbook vendor/apkp/site.yml \
  -i inventory/hosts.yml \
  --limit <host> \
  --tags deploy \
  -e apkp_apps_dir="$PWD/apps"

How to Test

See docs/testing/test-plan.md for the full test plan.

Quick sanity check after deployment:

# On the target host
systemctl status sample-app.service
podman ps

For detailed operator workflows (install, update, remove, migrate), see docs/deliverables/admin-guide.md. A Russian variant is available at docs/deliverables/admin-guide.ru.md.

Known Limitations

Limitation Details
No static IP for containers IP=/IP6= directives in Quadlet .kube are not supported in Podman 5.4.2. Containers receive dynamic IPs from the configured iprange pool.
InterfaceName= unsupported Quadlet .network files do not support InterfaceName= in Podman 5.4.2. The network is created via CLI with --interface-name.
Transient systemd units Quadlet-generated services are transient — systemctl enable has no effect. Boot persistence is handled via [Install] WantedBy=multi-user.target in the .kube file.
Single YAML file per app Quadlet's Yaml= directive supports only one file. All K8s manifests for an app are assembled into one multi-document YAML.
No PVC / named volumes Only hostPath bind-mounts are used.
Rootful Podman only All containers run under rootful Podman; user namespace remapping (userns) is not used. App process isolation is achieved via securityContext (runAsUser/runAsGroup).