# ansible-role-owncast Reusable Ansible role for deploying [Owncast](https://owncast.online/) on an existing Kubernetes cluster. Extracted from ansible-role-eom; no EOM services, domains, certificate issuers, or Tor installation are assumed by role defaults. ## Requirements - Ansible Core 2.16–2.19 on a Linux/macOS controller, WSL, or a Linux container. - Install [requirements.txt](requirements.txt) with pip and the collection with `ansible-galaxy collection install -r requirements.yml`. Dependencies belong in the interpreter Ansible uses on localhost. - An authorized kubeconfig or the usual `K8S_AUTH_*` authentication environment settings supported by `kubernetes.core.k8s`. - A default StorageClass supporting ReadWriteOnce, an explicit StorageClass, or an existing PVC in the instance namespace. Use encrypted backing storage and arrange encrypted backups separately. - For optional ingress: DNS, an ingress controller, and a valid TLS secret or cert-manager with a configured ClusterIssuer. For LoadBalancer services: a working cluster load balancer implementation. - Optional onion access requires the Tor operator and its `tor.k8s.torproject.org/v1alpha2` OnionService CRD already installed. Check out the repository as `ansible-role-owncast` under a directory in `ANSIBLE_ROLES_PATH`. For sibling role repositories, set that environment variable to their parent directory. The supplied playbooks run on the controller using its current Kubernetes context, with no SSH target required. Deployment commands below run from this repository on the controller. ## Your existing eom.dev instance Use [playbooks/deploy-eom.yml](playbooks/deploy-eom.yml). It explicitly preserves: - Namespace, Deployment, Service, PVC, and TLS secret named `owncast`. - Image `owncast/owncast:0.3.0`, one replica, 128Gi ReadWriteOnce storage at `/app/data`, CPU request `1.5`, HTTP 8080 and RTMP 1935. - LoadBalancer service with `externalTrafficPolicy: Local`. - NGINX ingress for `owncast.eom.dev`, with ClusterIssuer `ca-issuer`. - OnionService `http` in namespace `owncast`, port 80 → HTTP 8080, formerly part of the shared EOM onion task. First run `ansible-playbook -i localhost, playbooks/deploy-eom.yml --check --diff`, review the changes, then run `ansible-playbook -i localhost, playbooks/deploy-eom.yml`. If Tor was never installed, add `-e owncast_onion_enabled=false` to both commands. ### Migration safety 1. Back up the data volume securely and record the live image, resource names, StorageClass, selector, and any provider annotations before deploying. 2. Check the Kubernetes context. Keep the same namespace, PVC, and selector `app: owncast`. A different live immutable Deployment selector needs manual reconciliation; the role does not force-replace resources. 3. Stop invoking the retired EOM Owncast task. Its deployment code, full-deployment entry, and Owncast-specific onion task have been removed. The old task path only fails with migration guidance; it does not delegate or deploy anything. No automatic dependency is added; use this playbook. 4. Do not delete the existing PVC or TLS secret. Resources are adopted by name; administrator settings, stream keys, and the data directory are not reset. Leave `owncast_storage_class` blank to preserve the existing claim's class. 5. Verify the rollout, HTTPS, administrator login, RTMP ingest, and optional onion URL after deployment. The Deployment API is corrected from the old invalid `v1` to `apps/v1`. Updates use `Recreate` rather than overlapping replicas so the SQLite database and RWO volume have a single writer; an explicit null removes inherited `rollingUpdate` settings when patching the existing Deployment. Expect brief downtime during an update. Removing Ansible tasks does not delete any live cluster resources. ## Your friend's instance Use [playbooks/deploy-friend.yml](playbooks/deploy-friend.yml). It uses separate namespace, resource names, PVC, and TLS secret `owncast-friend`, with the same image, storage sizing, resource request, and network layout as your instance. Tor is disabled. Two required inputs deliberately have no invented defaults: - `friend_owncast_hostname`: your friend's real streaming hostname. - `friend_owncast_cluster_issuer`: a ClusterIssuer valid for that hostname. Define these in inventory variables for localhost or an external YAML vars file. Run `ansible-playbook -i localhost, playbooks/deploy-friend.yml -e @/secure/friend-vars.yml`. Use `--check --diff` first. Add `--ask-vault-pass` for a Vault-encrypted vars file; do not commit secrets or pass passwords on the command line. Extra variables can also override the `owncast_*` settings when different sizing is needed. The playbook rejects missing inputs, the eom.dev hostname, and the existing namespace before changing the cluster. For deployment on another cluster, use a separate kubeconfig/context. On a shared cluster, plan for another load balancer address and volume. Namespace separation is not a tenant security boundary by itself. ## Generic usage and variables Invoke `ansible-role-owncast` from your own playbook or inventory; neither site-specific playbook is required. All defaults are in [defaults/main.yml](defaults/main.yml). | Variable | Default / meaning | | --- | --- | | `owncast_namespace`, `owncast_name` | `owncast`; change for independent instances | | `owncast_image` | `owncast/owncast:0.3.0`; override with a tested tag or digest | | `owncast_image_pull_policy`, `owncast_image_pull_secrets` | `IfNotPresent`, `[]`; Secret references for private registries | | `owncast_resources` | Requests of 250m CPU and 256Mi memory; configure requests/limits for your transcoding workload | | `owncast_node_selector`, `owncast_tolerations` | `{}`, `[]` | | `owncast_pvc_name` | Resource name | | `owncast_existing_claim` | Empty; nonempty skips PVC creation and mounts that existing claim in this namespace | | `owncast_storage_size`, `owncast_storage_class` | `10Gi`, empty (default StorageClass) | | `owncast_service_type` | `ClusterIP`; also supports NodePort and LoadBalancer | | `owncast_service_annotations` | `{}`; provider-specific configuration | | `owncast_external_traffic_policy` | `Local`; emitted only for NodePort/LoadBalancer | | `owncast_load_balancer_source_ranges` | `[]`; optional provider-supported CIDR restrictions on both service ports | | `owncast_ingress_enabled`, `owncast_hostname` | `false`, empty; public HTTP exposure is opt-in | | `owncast_ingress_class`, `owncast_ingress_annotations` | Empty (controller default), `{}` | | `owncast_tls_enabled`, `owncast_tls_secret_name` | `true`, resource name; applies when ingress is enabled | | `owncast_cluster_issuer` | Empty; provide a name to use cert-manager, otherwise provision the TLS secret yourself | | `owncast_onion_enabled`, `owncast_onion_name` | `false`, `http` | HTTP and RTMP container/service ports remain fixed at 8080 and 1935; configure external TCP routing separately if not using a LoadBalancer/NodePort service. One Owncast process per data volume is supported; horizontal scaling is not. Storage expansion depends on the StorageClass; never shrink an existing claim or change its immutable StorageClass. Changing namespaces/names creates another instance, not a data migration. Disabling ingress or Tor stops managing it; it **does not delete a previously created resource**. Remove stale resources manually when revoking public access. The role uses only `state: present` and provides no destructive purge task. ## Security and operations - No secrets are embedded, generated, or rotated. For a fresh instance, first deploy privately (`owncast_service_type=ClusterIP`, ingress disabled) and use Owncast's supported admin workflow to set unique administrator credentials and stream key **before exposing it publicly**. Existing credentials remain in the persistent database and are not altered by this role. - The site playbooks intentionally preserve existing LoadBalancer behavior: **unencrypted HTTP 8080 is exposed alongside RTMP 1935**, bypassing ingress TLS on that port. Restrict access with provider/firewall rules or source ranges, or use ClusterIP plus separately configured RTMP routing. RTMP itself is unencrypted; use trusted networking/tunnels when confidentiality matters. - TLS terminates at ingress. Use a trusted certificate/issuer; Kubernetes API certificate verification is not disabled. Storage encryption, backups, RBAC, NetworkPolicies, ingress admin restrictions, and renewal are operator duties. - Pods receive no Kubernetes API token, cannot escalate privileges, drop Linux capabilities, and use runtime-default seccomp. UID and data ownership are intentionally not changed during migration. - The old image version is retained for migration compatibility, not presented as the latest release. Review and test image upgrades separately after a backup; a digest-pinned image can be supplied via `owncast_image`. ## Tests (no cluster needed) [tests/test.yml](tests/test.yml) renders templates with actual playbook vars, checks migration identity/settings and friend isolation, exercises optional fields, and checks invalid-input rejection. [tests/run.sh](tests/run.sh) also syntax-checks both playbooks and tests friend preflight failures before any cluster access. Run `sh tests/run.sh` on a configured controller, or build [tests/Dockerfile](tests/Dockerfile) with `docker build -f tests/Dockerfile -t owncast-role-tests .` and run `docker run --rm --network none --cap-drop ALL --security-opt no-new-privileges owncast-role-tests`. The image runs tests as a non-root user and requires no kubeconfig or secrets. ## License BSD-3-Clause; see [LICENSE](LICENSE).