Initial commit with the existing deployment files
Validate and deploy Plex / Validate manifests and scripts (push) Successful in 12s
Validate and deploy Plex / Deploy to Kubernetes (push) Successful in 18s

This commit is contained in:
2026-10-09 09:09:31 +02:00
parent 13c5a5a6e8
commit 35ae5557b9
10 changed files with 829 additions and 2 deletions
+76
View File
@@ -0,0 +1,76 @@
name: Validate and deploy Plex
on:
push:
pull_request:
env:
KUBECTL_VERSION: v1.36.4
KUBERNETES_API: https://host.containers.internal:6443
KUBERNETES_TLS_SERVER_NAME: 192.168.0.2
jobs:
validate:
name: Validate manifests and scripts
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Validate
run: ./test.sh
deploy:
name: Deploy to Kubernetes
if: gitea.ref == 'refs/heads/main'
needs: validate
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Install kubectl
run: |
curl --fail --silent --show-error --location \
--output "${RUNNER_TEMP}/kubectl" \
"https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
curl --fail --silent --show-error --location \
--output "${RUNNER_TEMP}/kubectl.sha256" \
"https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256"
printf '%s %s\n' "$(cat "${RUNNER_TEMP}/kubectl.sha256")" "${RUNNER_TEMP}/kubectl" | sha256sum --check
chmod 0700 "${RUNNER_TEMP}/kubectl"
- name: Configure Kubernetes access
env:
KUBE_CONFIG_BASE64: ${{ secrets.KUBE_CONFIG_BASE64 }}
run: |
test -n "${KUBE_CONFIG_BASE64}"
printf '%s' "${KUBE_CONFIG_BASE64}" | base64 --decode > "${RUNNER_TEMP}/kubeconfig"
chmod 0600 "${RUNNER_TEMP}/kubeconfig"
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
"${RUNNER_TEMP}/kubectl" config set-cluster cluster \
--server="${KUBERNETES_API}" \
--tls-server-name="${KUBERNETES_TLS_SERVER_NAME}"
- name: Apply and verify
env:
COMMIT_SHA: ${{ gitea.sha }}
run: |
set -euo pipefail
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
sed "s|deployment.brunner.ninja/revision: manual|deployment.brunner.ninja/revision: ${COMMIT_SHA}|" \
plex.yaml > "${RUNNER_TEMP}/plex.yaml"
"${RUNNER_TEMP}/kubectl" apply --dry-run=server --validate=false \
--filename "${RUNNER_TEMP}/plex.yaml"
"${RUNNER_TEMP}/kubectl" apply --validate=false \
--filename "${RUNNER_TEMP}/plex.yaml"
for attempt in {1..180}; do
IFS='|' read -r generation observed desired updated ready available image <<< "$(
"${RUNNER_TEMP}/kubectl" --namespace plex get deployment/plex \
--output=jsonpath='{.metadata.generation}|{.status.observedGeneration}|{.spec.replicas}|{.status.updatedReplicas}|{.status.readyReplicas}|{.status.availableReplicas}|{.spec.template.spec.containers[?(@.name=="plex")].image}'
)"
echo "Rollout ${attempt}/180: generation ${observed}/${generation}, replicas ${updated}/${desired} updated, ${ready}/${desired} ready, ${available}/${desired} available, image ${image}"
if [[ "${observed}" == "${generation}" && "${updated}" == "${desired}" \
&& "${ready}" == "${desired}" && "${available}" == "${desired}" ]]; then
exit 0
fi
sleep 5
done
"${RUNNER_TEMP}/kubectl" --namespace plex get deployment/plex --output=yaml
exit 1
+64 -2
View File
@@ -1,3 +1,65 @@
# plex-deployment
# Plex on Kubernetes
My kubernetes configuration files to run plex in my kubernetes cluster
This project runs the existing Plex server as a single high-performance Pod on
`arschrock` in its dedicated `plex` namespace. It is intentionally not highly
available and can never fail over to an ODROID. CPU and memory have scheduling
requests but no limits.
The 500 TB media tree stays on the host and is mounted read-only from
`/komposthaufen/multimedia`. The Pod also requires the existing `Videos`
directory and an init container verifies that the path is backed by bcachefs.
If the encrypted filesystem has not been unlocked and mounted, Plex does not
start. The Deployment also requires the explicit
`media.brunner.ninja/multimedia-ready=true` node label. The shared
`../media-storage-offline.sh` helper removes it and stops the media Pods before
unmounting; `../media-storage-online.sh` validates the mount before restoring
the label.
Persistent Plex configuration, metadata, preview images, and SQLite databases
live on the retained 160 GiB `plex-config` RBD PVC. Transcodes use node-local
ephemeral storage. The official Plex image is pinned to the same version as the
migrated Arch installation.
## One-time migration
`migrate.sh` stops the Arch service, confirms no Plex process remains, checks
both source SQLite databases, creates the PVC, and starts a one-shot copy Job:
```sh
./migrate.sh
kubectl -n plex logs -f job/plex-archlinux-migration
kubectl -n plex wait --for=condition=complete \
job/plex-archlinux-migration --timeout=12h
```
The Job refuses to overwrite a non-empty destination and compares regular-file
counts and byte totals before writing its completion marker. Copy I/O is
deliberately duty-cycle throttled so the migration cannot monopolize bcachefs
and Ceph. Do not delete the source `/var/lib/plex` tree until the Kubernetes
server has been verified and a separate backup exists.
Deploy with `./install.sh`. Plex remains available at
`https://plex.brunner.ninja` and directly on `192.168.0.2:32400`.
## Monitoring
The Pod contains a rootless Plex exporter. Its token is read from the migrated
`Preferences.xml` into a memory-backed volume; it is not duplicated in a
Kubernetes Secret or environment variable. A `ServiceMonitor` and alerts cover
the exporter, Plex API access, restart loops, and PVC capacity. Existing
Tautulli, node-exporter, Netdata, SMART, and bcachefs monitoring remain in use.
## Gitea CI/CD bootstrap
After initializing this directory as its own repository and creating the Gitea
repository, add the remote and perform the first push. Then run:
```sh
./create-ci-kubeconfig.sh
```
Store the single output line as `KUBE_CONFIG_BASE64`. The CI identity can only
update the already-created Plex resources named in `ci-deployer.yaml`; it
cannot read Secrets, run migration Jobs, or alter other workloads. Pull
requests validate, while pushes to `main` validate and deploy the pinned
upstream images.
+60
View File
@@ -0,0 +1,60 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: plex-deployer
namespace: plex
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: plex-deployer
namespace: plex
rules:
- apiGroups: [""]
resources: ["configmaps"]
resourceNames: ["plex-scripts"]
verbs: ["get", "patch", "update"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
resourceNames: ["plex-config"]
verbs: ["get", "patch", "update"]
- apiGroups: [""]
resources: ["services"]
resourceNames: ["plex"]
verbs: ["get", "patch", "update"]
- apiGroups: ["apps"]
resources: ["deployments"]
resourceNames: ["plex"]
verbs: ["get", "patch", "update"]
- apiGroups: ["traefik.io"]
resources: ["ingressroutes"]
resourceNames: ["plex-brunner-ninja"]
verbs: ["get", "patch", "update"]
- apiGroups: ["monitoring.coreos.com"]
resources: ["servicemonitors", "prometheusrules"]
resourceNames: ["plex"]
verbs: ["get", "patch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: plex-deployer
namespace: plex
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: plex-deployer
subjects:
- kind: ServiceAccount
name: plex-deployer
namespace: plex
---
apiVersion: v1
kind: Secret
metadata:
name: plex-deployer-token
namespace: plex
annotations:
kubernetes.io/service-account.name: plex-deployer
type: kubernetes.io/service-account-token
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
namespace=plex
service_account=plex-deployer
secret=plex-deployer-token
if [[ "$(kubectl config current-context)" != kubernetes-admin@kubernetes ]]; then
echo 'Run this only with the kubernetes-admin@kubernetes homelab context.' >&2
exit 1
fi
kubectl apply --filename "${project_dir}/namespace.yaml" >&2
kubectl apply --filename "${project_dir}/ci-deployer.yaml" >&2
for attempt in {1..30}; do
if kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.token}' 2>/dev/null | grep -q .; then
break
fi
if [[ "${attempt}" == 30 ]]; then
echo 'Timed out waiting for the service-account token.' >&2
exit 1
fi
sleep 1
done
workdir=$(mktemp --directory)
trap 'rm -rf "${workdir}"' EXIT
server=$(kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}')
kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.ca\.crt}' | base64 --decode > "${workdir}/ca.crt"
token=$(kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.token}' | base64 --decode)
export KUBECONFIG="${workdir}/config"
kubectl config set-cluster cluster --server="${server}" \
--certificate-authority="${workdir}/ca.crt" --embed-certs=true >/dev/null
kubectl config set-credentials "${service_account}" --token="${token}" >/dev/null
kubectl config set-context plex --cluster=cluster --user="${service_account}" \
--namespace="${namespace}" >/dev/null
kubectl config use-context plex >/dev/null
echo 'Store this single line as the Gitea Actions secret KUBE_CONFIG_BASE64.' >&2
echo 'Treat it as a password; do not commit it.' >&2
base64 --wrap=0 "${KUBECONFIG}"
printf '\n'
Executable
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
set -eu
project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
expected_context=kubernetes-admin@kubernetes
context=$(kubectl config current-context)
if [ "${context}" != "${expected_context}" ]; then
echo "Expected Kubernetes context ${expected_context}, found ${context}." >&2
exit 1
fi
if [ "$(findmnt -T /komposthaufen/multimedia -n -o FSTYPE)" != bcachefs ]; then
echo '/komposthaufen/multimedia is not backed by bcachefs.' >&2
exit 1
fi
test -d /komposthaufen/multimedia/Videos
kubectl apply --filename "${project_dir}/namespace.yaml"
kubectl --namespace plex wait --for=condition=complete \
job/plex-archlinux-migration --timeout=5s
kubectl apply --dry-run=server --filename "${project_dir}/plex.yaml"
kubectl apply --filename "${project_dir}/plex.yaml"
kubectl --namespace plex rollout status deployment/plex --timeout=30m
pod=$(kubectl --namespace plex get pod \
--selector=app.kubernetes.io/name=plex,app.kubernetes.io/component=server \
--output=jsonpath='{.items[0].metadata.name}')
node=$(kubectl --namespace plex get pod "${pod}" --output=jsonpath='{.spec.nodeName}')
image=$(kubectl --namespace plex get deployment plex --output=jsonpath='{.spec.template.spec.containers[?(@.name=="plex")].image}')
test "${node}" = arschrock
kubectl --namespace plex get pod "${pod}" --output=wide
printf 'Plex is ready on %s with image %s.\n' "${node}" "${image}"
Executable
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
expected_context=kubernetes-admin@kubernetes
context=$(kubectl config current-context)
if [[ "${context}" != "${expected_context}" ]]; then
echo "Expected Kubernetes context ${expected_context}, found ${context}." >&2
exit 1
fi
sudo systemctl stop plexmediaserver.service
if sudo systemctl is-active --quiet plexmediaserver.service; then
echo 'plexmediaserver.service is still active; refusing to copy its database.' >&2
exit 1
fi
if pgrep -u plex >/dev/null; then
echo 'A process owned by plex is still running; refusing to copy its database.' >&2
exit 1
fi
database_root='/var/lib/plex/Plex Media Server/Plug-in Support/Databases'
for database in \
"${database_root}/com.plexapp.plugins.library.db" \
"${database_root}/com.plexapp.plugins.library.blobs.db"; do
result=$(sudo -u plex '/usr/lib/plexmediaserver/Plex SQLite' "${database}" 'PRAGMA quick_check;')
if [[ "${result}" != ok ]]; then
echo "SQLite quick_check failed for ${database}: ${result}" >&2
exit 1
fi
done
kubectl apply --filename "${project_dir}/namespace.yaml"
kubectl apply --filename "${project_dir}/plex.yaml" \
--selector=app.kubernetes.io/component=state
kubectl --namespace plex wait \
--for=jsonpath='{.status.phase}'=Bound \
persistentvolumeclaim/plex-config \
--timeout=10m
if kubectl --namespace plex get job plex-archlinux-migration >/dev/null 2>&1; then
echo 'Job plex-archlinux-migration already exists; inspect it instead of overwriting it.' >&2
exit 1
fi
kubectl apply --filename "${project_dir}/migration-job.yaml"
echo 'Migration started. Follow it with:'
echo ' kubectl -n plex logs -f job/plex-archlinux-migration'
echo ' kubectl -n plex wait --for=condition=complete job/plex-archlinux-migration --timeout=12h'
+86
View File
@@ -0,0 +1,86 @@
apiVersion: batch/v1
kind: Job
metadata:
name: plex-archlinux-migration
namespace: plex
labels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: migration
spec:
backoffLimit: 0
template:
metadata:
labels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: migration
spec:
restartPolicy: Never
nodeSelector:
kubernetes.io/hostname: arschrock
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
containers:
- name: copy
image: docker.io/library/alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
imagePullPolicy: IfNotPresent
command:
- /bin/sh
- -ec
- |
marker=/target/.archlinux-migration-complete
source='/source/Plex Media Server'
destination='/target/Library/Application Support/Plex Media Server'
test -d "${source}"
if [ -e "${marker}" ]; then
echo 'Migration marker already exists; refusing to overwrite the PVC.' >&2
exit 1
fi
if find /target -mindepth 1 -maxdepth 1 ! -name lost+found -print -quit | grep -q .; then
echo 'The destination PVC is not empty; refusing to overwrite it.' >&2
exit 1
fi
mkdir -p '/target/Library/Application Support'
cp -a "${source}" '/target/Library/Application Support/' &
copy_pid=$!
while kill -0 "${copy_pid}" 2>/dev/null; do
kill -CONT "${copy_pid}" 2>/dev/null || true
sleep 0.2
kill -STOP "${copy_pid}" 2>/dev/null || true
sleep 3.8
done
kill -CONT "${copy_pid}" 2>/dev/null || true
wait "${copy_pid}"
sync -f /target
source_files=$(find "${source}" -type f | wc -l)
destination_files=$(find "${destination}" -type f | wc -l)
source_bytes=$(find "${source}" -type f -exec stat -c '%s' {} + | awk '{ total += $1 } END { print total + 0 }')
destination_bytes=$(find "${destination}" -type f -exec stat -c '%s' {} + | awk '{ total += $1 } END { print total + 0 }')
test "${source_files}" = "${destination_files}"
test "${source_bytes}" = "${destination_bytes}"
printf 'source_files=%s\nsource_bytes=%s\n' "${source_files}" "${source_bytes}" > "${marker}"
chown 964:964 /target "${marker}"
echo "Copied ${source_files} files (${source_bytes} bytes)."
resources:
requests:
cpu: 250m
memory: 256Mi
securityContext:
allowPrivilegeEscalation: false
runAsUser: 0
runAsGroup: 0
volumeMounts:
- name: source
mountPath: /source
readOnly: true
- name: target
mountPath: /target
volumes:
- name: source
hostPath:
path: /var/lib/plex
type: Directory
- name: target
persistentVolumeClaim:
claimName: plex-config
+6
View File
@@ -0,0 +1,6 @@
apiVersion: v1
kind: Namespace
metadata:
name: plex
labels:
app.kubernetes.io/name: plex
+385
View File
@@ -0,0 +1,385 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: plex-config
namespace: plex
labels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: state
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 160Gi
storageClassName: rook-ceph-block-ec
---
apiVersion: v1
kind: ConfigMap
metadata:
name: plex-scripts
namespace: plex
labels:
app.kubernetes.io/name: plex
data:
check-media.sh: |
#!/bin/sh
set -eu
filesystem_magic=$(stat -f -c '%t' /komposthaufen/multimedia)
if [ "${filesystem_magic}" != ca451a4e ]; then
echo "Expected bcachefs at /komposthaufen/multimedia, found filesystem magic ${filesystem_magic}" >&2
exit 1
fi
test -d /komposthaufen/multimedia/Videos
extract-token.sh: |
#!/bin/sh
set -eu
preferences='/config/Library/Application Support/Plex Media Server/Preferences.xml'
test -s "${preferences}"
token=$(sed -n 's/.*PlexOnlineToken="\([^"]*\)".*/\1/p' "${preferences}")
if [ -z "${token}" ]; then
echo 'PlexOnlineToken is missing from Preferences.xml' >&2
exit 1
fi
umask 077
printf '%s' "${token}" > /token/plex-token
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: plex
namespace: plex
labels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: server
annotations:
deployment.brunner.ninja/revision: manual
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: server
template:
metadata:
labels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: server
spec:
automountServiceAccountToken: false
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
nodeSelector:
kubernetes.io/hostname: arschrock
media.brunner.ninja/multimedia-ready: "true"
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
terminationGracePeriodSeconds: 120
securityContext:
seccompProfile:
type: RuntimeDefault
initContainers:
- name: media-ready
image: docker.io/library/alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
imagePullPolicy: IfNotPresent
command:
- /tools/check-media.sh
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
volumeMounts:
- name: media
mountPath: /komposthaufen/multimedia
readOnly: true
- name: media-ready
mountPath: /media-ready
readOnly: true
- name: tools
mountPath: /tools
readOnly: true
- name: exporter-token
image: docker.io/library/alpine@sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b
imagePullPolicy: IfNotPresent
command:
- /tools/extract-token.sh
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 964
runAsGroup: 964
volumeMounts:
- name: config
mountPath: /config
readOnly: true
- name: exporter-token
mountPath: /token
- name: tools
mountPath: /tools
readOnly: true
containers:
- name: plex
image: docker.io/plexinc/pms-docker@sha256:f6748983db1054b571b57b4a40f07f53af6c4bfb9edd1fa455f5ebb6e16449bc
imagePullPolicy: IfNotPresent
env:
- name: TZ
value: Europe/Vienna
- name: PLEX_UID
value: "964"
- name: PLEX_GID
value: "964"
- name: CHANGE_CONFIG_DIR_OWNERSHIP
value: "false"
ports:
- name: http
containerPort: 32400
protocol: TCP
startupProbe:
httpGet:
path: /identity
port: http
failureThreshold: 180
periodSeconds: 5
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /identity
port: http
failureThreshold: 3
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /identity
port: http
failureThreshold: 3
periodSeconds: 30
timeoutSeconds: 5
resources:
requests:
cpu: 500m
memory: 2Gi
ephemeral-storage: 1Gi
securityContext:
allowPrivilegeEscalation: false
volumeMounts:
- name: config
mountPath: /config
- name: media
mountPath: /komposthaufen/multimedia
readOnly: true
- name: transcode
mountPath: /transcode
- name: plex-exporter
image: ghcr.io/cplieger/plex-exporter@sha256:07ee6d213698bfb0dc086004f26e1048380caaa229f0429e9aafa2eb487ee826
imagePullPolicy: IfNotPresent
env:
- name: PLEX_URL
value: http://127.0.0.1:32400
- name: PLEX_TOKEN_FILE
value: /token/plex-token
ports:
- name: metrics
containerPort: 9594
protocol: TCP
startupProbe:
httpGet:
path: /api/health
port: metrics
failureThreshold: 60
periodSeconds: 5
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /api/health
port: metrics
failureThreshold: 3
periodSeconds: 10
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /api/health
port: metrics
failureThreshold: 3
periodSeconds: 30
timeoutSeconds: 5
resources:
requests:
cpu: 10m
memory: 32Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 964
runAsGroup: 964
volumeMounts:
- name: exporter-token
mountPath: /token
readOnly: true
- name: exporter-tmp
mountPath: /tmp
volumes:
- name: config
persistentVolumeClaim:
claimName: plex-config
- name: media
hostPath:
path: /komposthaufen/multimedia
type: Directory
- name: media-ready
hostPath:
path: /komposthaufen/multimedia/Videos
type: Directory
- name: transcode
emptyDir: {}
- name: exporter-token
emptyDir:
medium: Memory
sizeLimit: 1Mi
- name: exporter-tmp
emptyDir:
medium: Memory
sizeLimit: 8Mi
- name: tools
configMap:
name: plex-scripts
defaultMode: 0555
---
apiVersion: v1
kind: Service
metadata:
name: plex
namespace: plex
labels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: service
spec:
selector:
app.kubernetes.io/name: plex
app.kubernetes.io/component: server
ports:
- name: http
port: 32400
targetPort: http
protocol: TCP
- name: metrics
port: 9594
targetPort: metrics
protocol: TCP
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: plex-brunner-ninja
namespace: plex
labels:
app.kubernetes.io/name: plex
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`plex.brunner.ninja`)
services:
- kind: Service
name: plex
port: http
passHostHeader: true
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: plex
namespace: plex
labels:
app.kubernetes.io/name: plex
spec:
selector:
matchLabels:
app.kubernetes.io/name: plex
app.kubernetes.io/component: service
endpoints:
- port: metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s
metricRelabelings:
- action: labeldrop
regex: (pod|endpoint|container)
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: plex
namespace: plex
labels:
app.kubernetes.io/name: plex
spec:
groups:
- name: plex.availability
rules:
- alert: PlexMetricsTargetDown
expr: |-
max(kube_deployment_status_replicas_available{namespace="plex",deployment="plex"}) >= 1
and on()
(
max(up{namespace="plex",service="plex"}) < 1
or absent(up{namespace="plex",service="plex"})
)
for: 10m
labels:
severity: warning
component: exporter
annotations:
summary: Plex metrics are unavailable
description: The Plex Pod is available, but Prometheus cannot scrape its exporter.
- alert: PlexAPIUnreachable
expr: max(plex_http_reachable{namespace="plex",service="plex"}) < 1
for: 10m
labels:
severity: warning
component: plex
annotations:
summary: Plex API is unreachable from its exporter
description: The exporter has failed to poll Plex for at least ten minutes.
- alert: PlexContainerRestarting
expr: |-
sum by (container) (
increase(kube_pod_container_status_restarts_total{namespace="plex",pod=~"plex-.*"}[15m])
) > 2
for: 5m
labels:
severity: warning
component: kubernetes
annotations:
summary: Plex container is repeatedly restarting
description: Container {{ $labels.container }} restarted more than twice in 15 minutes.
- alert: PlexConfigVolumeFilling
expr: |-
kubelet_volume_stats_available_bytes{namespace="plex",persistentvolumeclaim="plex-config"}
/
kubelet_volume_stats_capacity_bytes{namespace="plex",persistentvolumeclaim="plex-config"} < 0.15
for: 30m
labels:
severity: warning
component: storage
annotations:
summary: Plex configuration volume is filling
description: Less than 15 percent of the Plex configuration PVC remains available.
Executable
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
sh -n "${project_dir}/install.sh"
bash -n "${project_dir}/migrate.sh"
bash -n "${project_dir}/create-ci-kubeconfig.sh"
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
for manifest in namespace.yaml plex.yaml migration-job.yaml; do
docker run --rm --interactive \
ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c \
-strict -ignore-missing-schemas -summary < "${project_dir}/${manifest}"
done
docker run --rm --interactive \
ghcr.io/yannh/kubeconform:v0.7.0@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c \
-strict -summary < "${project_dir}/ci-deployer.yaml"
fi