- Jinja 100%
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. |
||
|---|---|---|
| .ansible/test-logs | ||
| apps | ||
| docs | ||
| inventory | ||
| memory-bank | ||
| playbooks | ||
| roles | ||
| .clinerules | ||
| .gitignore | ||
| ansible.cfg | ||
| deploy_app.yml | ||
| README.md | ||
| README.ru.md | ||
| site.yml | ||
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/, realapps/, secrets, and runbooks, while APKP is pinned undervendor/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 where →
host_appslists ininventory/host_vars/<host>.yml. - What data exists → on the target hosts under
/opt/apps/<name>/. Data is never deleted by Ansible unlessremove_data=trueis 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
-
Create
apps/<name>/app.ymlwith 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 -
Create the template
apps/<name>/templates/pod.yaml.j2:- Use
{{ app_uid }}/{{ app_gid }}insecurityContext - Use
{{ app_vars.* }}for customizable values - Use
hostPathvolumes pointing to{{ podman_app_deploy_base_dir }}/{{ app_name }}/<subdir> - All manifests for one app are concatenated into a single file — separate resources with
---
- Use
-
Add the application to
host_appsin the target host'shost_vars/<host>.yml:host_apps: - sample-app - my-app -
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). |