2026-08-22 15:57:58 +02:00
|
|
|
---
|
|
|
|
|
marp: true
|
|
|
|
|
theme: gaia
|
|
|
|
|
class: invert
|
|
|
|
|
paginate: true
|
|
|
|
|
title: How to implement a TAPPaaS module
|
2026-08-22 18:03:06 +02:00
|
|
|
description: Live build of a Podman module — OrangeMaker, 24 August
|
2026-08-22 15:57:58 +02:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
|
section { font-size: 26px; }
|
|
|
|
|
h1 { font-size: 1.5em; }
|
|
|
|
|
h2 { font-size: 1.15em; }
|
2026-08-22 16:47:53 +02:00
|
|
|
pre { font-size: 0.7em; line-height: 1.35; }
|
|
|
|
|
table { font-size: 0.78em; }
|
|
|
|
|
th, td { padding: 0.2em 0.55em; }
|
2026-08-22 15:57:58 +02:00
|
|
|
/* Unfinished content, loud on purpose: an unfinished deck should never be
|
|
|
|
|
presented by accident. Written in the markdown as `TODO: ...`. */
|
|
|
|
|
code.todo { color: #ff8a80; font-weight: 600; }
|
2026-08-22 16:47:53 +02:00
|
|
|
/* Mermaid renders at its natural size, which is far too small on a projector. */
|
|
|
|
|
div.mermaid { display: flex; justify-content: center; width: 100%; }
|
|
|
|
|
div.mermaid svg { width: 100% !important; height: auto !important; max-height: 500px; }
|
|
|
|
|
section.diagram h2 { margin-bottom: 0.1em; }
|
2026-08-22 15:57:58 +02:00
|
|
|
</style>
|
|
|
|
|
|
|
|
|
|
<!-- _paginate: false -->
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
# Getting a module onto TAPPaaS
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
Live build of a **Podman** container host
|
|
|
|
|
|
2026-08-22 18:03:06 +02:00
|
|
|
OrangeMaker · 24 August
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
<!--
|
2026-08-22 16:47:53 +02:00
|
|
|
Slides are the map; the terminal is the territory. Everything here has a live
|
|
|
|
|
counterpart — keep the deck moving and spend the time in the shell.
|
2026-08-22 15:57:58 +02:00
|
|
|
-->
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
## The plan
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 18:03:06 +02:00
|
|
|
1. **What TAPPaaS is** — a few diagrams, ten minutes, no deeper
|
2026-08-22 16:47:53 +02:00
|
|
|
2. **What a module actually is** — a json contract and three scripts
|
2026-08-22 18:03:06 +02:00
|
|
|
3. **Build one live** — the `lab1` environment, then `podman` into it
|
2026-08-22 16:47:53 +02:00
|
|
|
4. **Prove it works** — tests, then the web console
|
|
|
|
|
5. **You get a login** — your own identity on this system
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
You leave with: a mental model, a module you watched get built, and an account.
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
<!-- _class: invert diagram -->
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
## The shape of a TAPPaaS site
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
```mermaid
|
|
|
|
|
flowchart LR
|
|
|
|
|
sat["satellite VPS<br/>public IP · ingress<br/>off-site backup"]
|
|
|
|
|
subgraph z0["mgmt · zone mgmt"]
|
|
|
|
|
cicd["tappaas-cicd"]
|
|
|
|
|
net["network"]
|
|
|
|
|
clu["cluster"]
|
|
|
|
|
bak["backup"]
|
|
|
|
|
idp["identity"]
|
|
|
|
|
end
|
|
|
|
|
subgraph z1["default · zone srv"]
|
|
|
|
|
nc["nextcloud"]
|
|
|
|
|
ha["home-assistant"]
|
|
|
|
|
end
|
2026-08-22 18:03:06 +02:00
|
|
|
subgraph z2["lab1 · zone lab1"]
|
2026-08-22 16:47:53 +02:00
|
|
|
pod["podman — today"]
|
|
|
|
|
end
|
|
|
|
|
sat --> net
|
|
|
|
|
cicd --> nc
|
|
|
|
|
cicd --> pod
|
|
|
|
|
```
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
## The words we will use
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
| Word | Meaning |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| **Module** | The smallest deployable unit — its own VM, its own json contract. Foundation *and* apps are modules. |
|
2026-08-22 18:03:06 +02:00
|
|
|
| **Environment** | A tenant. `mgmt` and one named after the site always exist; add more freely. Today we add `lab1`. |
|
2026-08-22 16:47:53 +02:00
|
|
|
| **Zone** | A VLAN with a firewall policy. Modules land in their environment's zone. |
|
|
|
|
|
| **Mothership** | `tappaas-cicd` — where the managers run and where you type. |
|
|
|
|
|
| **Satellite** | Optional VPS, for sites with no usable public IP. |
|
|
|
|
|
|
|
|
|
|
Everything is a module. There is one model to learn, not eight.
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
<!-- _class: invert diagram -->
|
|
|
|
|
|
2026-08-22 18:03:06 +02:00
|
|
|
## Developing modules: dev instance, private branch
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
```mermaid
|
2026-08-22 16:47:53 +02:00
|
|
|
flowchart RL
|
2026-08-22 18:03:06 +02:00
|
|
|
subgraph Development["Development repository"]
|
|
|
|
|
dev_main["main"]
|
|
|
|
|
end
|
|
|
|
|
subgraph Upstream["Upstream repository"]
|
|
|
|
|
up_main["main"]
|
|
|
|
|
up_stable["stable"]
|
|
|
|
|
up_main -->|merge| up_stable
|
|
|
|
|
end
|
|
|
|
|
subgraph InstanceDev["TAPPaaS instance · dev"]
|
|
|
|
|
localDev["clone of Upstream"]
|
|
|
|
|
localDev_dev["clone of Development"]
|
|
|
|
|
end
|
|
|
|
|
subgraph InstanceProd["TAPPaaS instance · prod"]
|
|
|
|
|
localProd["clone of Upstream"]
|
|
|
|
|
end
|
|
|
|
|
localProd -->|pull| up_stable
|
|
|
|
|
localDev -->|pull| up_main
|
|
|
|
|
localDev_dev -->|push| dev_main
|
|
|
|
|
dev_main -->|PR| up_main
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
<!-- _class: invert diagram -->
|
|
|
|
|
|
|
|
|
|
## Our setup tonight
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
flowchart RL
|
|
|
|
|
subgraph t["TAPPaaS · codeberg"]
|
2026-08-22 16:47:53 +02:00
|
|
|
stable["stable"]
|
|
|
|
|
main["main"]
|
|
|
|
|
end
|
2026-08-22 18:03:06 +02:00
|
|
|
subgraph c["Community · codeberg"]
|
2026-08-22 16:47:53 +02:00
|
|
|
cm["main"]
|
|
|
|
|
end
|
2026-08-22 18:03:06 +02:00
|
|
|
subgraph f["forgejo.makerfloss.eu"]
|
|
|
|
|
fm["main"]
|
|
|
|
|
end
|
|
|
|
|
subgraph inst["Our TAPPaaS · tappaas-cicd"]
|
2026-08-22 16:47:53 +02:00
|
|
|
lt["clone of TAPPaaS"]
|
|
|
|
|
lc["clone of Community"]
|
2026-08-22 18:03:06 +02:00
|
|
|
lf["clone of MakerFLOSS"]
|
2026-08-22 16:47:53 +02:00
|
|
|
end
|
2026-08-22 18:03:06 +02:00
|
|
|
lt -->|pull| main
|
2026-08-22 16:47:53 +02:00
|
|
|
lc -->|pull| cm
|
2026-08-22 18:03:06 +02:00
|
|
|
lf -->|pull| fm
|
|
|
|
|
classDef tracked stroke:#ff8a80,stroke-width:3px
|
|
|
|
|
class main tracked
|
2026-08-22 15:57:58 +02:00
|
|
|
```
|
|
|
|
|
|
2026-08-22 18:03:06 +02:00
|
|
|
We track **`main`**, not `stable` — this is a lab, we want the new things.
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
## A module is a contract and three scripts
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
podman/
|
|
|
|
|
├── podman.json # the contract — what it is, needs, provides
|
|
|
|
|
├── install.sh # put the software in the VM (once)
|
|
|
|
|
├── update.sh # keep it patched (on schedule)
|
|
|
|
|
├── test.sh # prove it still works (gates updates)
|
|
|
|
|
├── README.md # what it is
|
|
|
|
|
└── INSTALL.md # what automation cannot do for you
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
That is the whole surface. Everything else is the platform's job.
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## The contract, for real
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
```json
|
|
|
|
|
{
|
2026-08-22 16:47:53 +02:00
|
|
|
"description": "Podman container host — Debian 13 VM with rootless Podman + Cockpit",
|
|
|
|
|
"vmname": "podman", "vmid": 812,
|
2026-08-22 18:03:06 +02:00
|
|
|
"dependsOn": ["cluster:vm", "templates:debian", "backup:vm",
|
2026-08-22 20:42:04 +02:00
|
|
|
"network:proxy", "identity:accessControl"],
|
2026-08-22 16:47:53 +02:00
|
|
|
"config": {
|
|
|
|
|
"cluster:vm": { "cores": 2, "memory": "2048", "diskSize": "20G",
|
|
|
|
|
"image": "debian-13-generic-amd64.qcow2" },
|
|
|
|
|
"network:proxy": { "proxyPort": 9090, "proxyUpstreamTls": true,
|
|
|
|
|
"proxyUpstreamHttp1": true, "proxyAllowedZones": ["mgmt"] }
|
|
|
|
|
}
|
2026-08-22 15:57:58 +02:00
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-22 18:03:06 +02:00
|
|
|
`dependsOn` is the whole trick: five services, declared — not scripted.
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 20:42:04 +02:00
|
|
|
## `install.sh` — called once, at add
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 20:42:04 +02:00
|
|
|
```bash
|
|
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
|
|
exec "${SCRIPT_DIR}/update.sh" "$@"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
By the time it runs, `cluster:vm` has built the VM and `templates:debian` has
|
|
|
|
|
apt-upgraded it and installed the guest agent.
|
|
|
|
|
|
|
|
|
|
For podman there is nothing install-only — **install *is* update**. Five lines is a
|
|
|
|
|
perfectly good `install.sh`.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## `update.sh` — where the work is
|
|
|
|
|
|
|
|
|
|
Runs on the **mothership**, not on the VM. It:
|
|
|
|
|
|
|
|
|
|
1. finds the node hosting the VM via `pvesh` — HA-safe, the VM may have moved
|
|
|
|
|
2. resolves the VM's IP through the **Proxmox guest agent**, then waits for SSH
|
|
|
|
|
3. `apt-get install podman podman-compose slirp4netns uidmap cockpit cockpit-podman`
|
|
|
|
|
4. enables the **rootless** `podman.socket`, `loginctl enable-linger`, `cockpit.socket`
|
|
|
|
|
5. records `/etc/tappaas-podman.version`
|
|
|
|
|
6. polls `https://localhost:9090` until the console answers, then prints the URL
|
|
|
|
|
|
|
|
|
|
Every step is re-runnable: apt is a no-op when current, the marker is overwritten,
|
|
|
|
|
the sockets are `enable --now`. That is what "idempotent" has to mean in practice.
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
## `test.sh` — the one that earns trust
|
|
|
|
|
|
|
|
|
|
| Check | How |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| VM reachable | IP resolves via the guest agent |
|
|
|
|
|
| podman installed | version marker + `podman --version` actually runs |
|
|
|
|
|
| plugin present | `dpkg -s cockpit-podman` |
|
|
|
|
|
| console alive | `:9090` answers `200`, `302`, `401` or `403` |
|
|
|
|
|
|
|
|
|
|
A login page **is** a pass — pre-auth, `401` is the healthy answer.
|
|
|
|
|
|
|
|
|
|
Prints `N passed, M failed` and exits non-zero on any failure. Note `curl -s`, not
|
|
|
|
|
`--fail`: `--fail` exits 22 on 4xx and would corrupt the `-w '%{http_code}'` we read.
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 20:42:04 +02:00
|
|
|
Run on demand, and again before every update is merged. If `test.sh` is honest,
|
|
|
|
|
unattended updates are safe — that is the entire deal.
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
## What you did not have to write
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
- A VM — `cluster:vm` built it from the Debian 13 cloud image
|
|
|
|
|
- OS prep — `templates:debian` did apt + guest agent
|
|
|
|
|
- A VLAN, an interface, DHCP, firewall rules — the zone came with the environment
|
2026-08-22 18:03:06 +02:00
|
|
|
- `https://podman.lab1.makerfloss.eu` with a real certificate — `network:proxy`
|
2026-08-22 20:42:04 +02:00
|
|
|
- An access gate — `identity:accessControl`: only the bound groups reach the URL
|
2026-08-22 18:03:06 +02:00
|
|
|
- Nightly backup to PBS, scheduled updates, health reporting
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 18:03:06 +02:00
|
|
|
A few lines of json bought all of it.
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
## Demo 1 — an environment of our own
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
```bash
|
2026-08-22 18:03:06 +02:00
|
|
|
network-manager zone add lab1 --from-zone srv
|
|
|
|
|
network-manager show lab1 # vlan tag, subnet, access-to
|
|
|
|
|
network-manager reconcile # dry-run: drift on all 4 planes
|
|
|
|
|
network-manager reconcile --apply # converge OPNsense, Proxmox, switch, AP
|
|
|
|
|
|
|
|
|
|
environment-manager add lab1 --display "MakerFLOSS lab" \
|
|
|
|
|
--zone lab1 --domain lab1.makerfloss.eu
|
|
|
|
|
environment-manager list
|
|
|
|
|
environment-manager show lab1
|
2026-08-22 16:47:53 +02:00
|
|
|
```
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 18:03:06 +02:00
|
|
|
A tenant with its own VLAN, firewall posture and DNS names —
|
|
|
|
|
`lab1.makerfloss.eu` outside, `lab1.internal` inside.
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
<!-- Show zones.json before/after, and the OPNsense interface appearing. -->
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
## Demo 2 — install the module
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
```bash
|
2026-08-22 18:03:06 +02:00
|
|
|
module-manager module add podman --environment lab1
|
|
|
|
|
|
|
|
|
|
module-manager module list # what is deployed
|
|
|
|
|
module-manager module show podman # the resolved config, cascade applied
|
2026-08-22 16:47:53 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Watch the order: dependencies resolve first, then the VM, then the network, then
|
|
|
|
|
`install.sh`. Install order is **computed** from every module's `dependsOn` — nobody
|
|
|
|
|
maintains a list.
|
|
|
|
|
|
|
|
|
|
<!-- Expect a few minutes. Good moment for questions. -->
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
## Demo 3 — prove it, then look at it
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
```bash
|
|
|
|
|
module-manager module test podman
|
|
|
|
|
```
|
|
|
|
|
|
2026-08-22 18:03:06 +02:00
|
|
|
Then open **`https://podman.lab1.makerfloss.eu`** — Cockpit, with the Podman page.
|
2026-08-22 16:47:53 +02:00
|
|
|
|
|
|
|
|
- Reachable from the `mgmt` zone only. Not from the internet, by declaration.
|
2026-08-22 20:42:04 +02:00
|
|
|
- Authentik gates the URL: no `devops` group, no console. Then Cockpit asks *again* —
|
|
|
|
|
it is not an OIDC client, and a local Unix account is not optional for it.
|
|
|
|
|
- Two logins, honestly. `DESIGN.md` in the repo says what it would take to fix.
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
## Where this module lives
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
```text
|
|
|
|
|
Community/src/larsrossen/containers/podman/
|
2026-08-22 15:57:58 +02:00
|
|
|
```
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
Three homes for a module, all first-class:
|
|
|
|
|
|
|
|
|
|
| Home | For |
|
|
|
|
|
| --- | --- |
|
|
|
|
|
| **TAPPaaS** repo, by pull request | modules the whole project should carry |
|
|
|
|
|
| **Community** repo | yours, shared, no gatekeeping — today's podman |
|
2026-08-22 18:03:06 +02:00
|
|
|
| **Private** repo — e.g. `forgejo.makerfloss.eu` | yours, not shared |
|
2026-08-22 16:47:53 +02:00
|
|
|
|
|
|
|
|
Adding a repository is one `site-manager repository add`.
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
## Your turn — identity on this TAPPaaS
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
```bash
|
|
|
|
|
people-manager user add <you> --email <you>@example.org \
|
2026-08-22 18:38:19 +02:00
|
|
|
--roles user --groups devops
|
2026-08-22 16:47:53 +02:00
|
|
|
people-manager reconcile # preview
|
|
|
|
|
people-manager reconcile --apply # push to Authentik
|
2026-08-22 18:03:06 +02:00
|
|
|
|
|
|
|
|
authentik-manager user-recovery-link <you> # one-time URL: set your own password
|
2026-08-22 16:47:53 +02:00
|
|
|
```
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 18:03:06 +02:00
|
|
|
One login per person, roles via groups. Nobody types a password into a chat window.
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
<!-- Do these live, one per participant, while the podman VM builds. -->
|
2026-08-22 15:57:58 +02:00
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
## Take it further
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
- **Develop a module** — tappaas.org/generated/develop-a-module
|
|
|
|
|
- **Every json field** — tappaas.org/generated/schemas
|
|
|
|
|
- **Zones and the access model** — tappaas.org/generated/zones
|
|
|
|
|
- **Repository topology** — tappaas.org/generated/design/cicd-git
|
|
|
|
|
- **This deck** — slides.makerfloss.eu/tappaas/how-to/new-module
|
2026-08-22 15:57:58 +02:00
|
|
|
|
2026-08-22 16:47:53 +02:00
|
|
|
Pick an app you want on a sovereign box. Open an issue first — someone may already
|
|
|
|
|
be packaging it.
|