First try with CI
Validate and deploy coturn / Validate manifests (push) Successful in 16s
Validate and deploy coturn / Deploy to Kubernetes (push) Canceled after 3m27s

This commit is contained in:
2026-08-24 10:51:38 +02:00
parent e2d21f0863
commit 22f0fce902
9 changed files with 584 additions and 2 deletions
+84
View File
@@ -0,0 +1,84 @@
name: Validate and deploy coturn
on:
push:
pull_request:
env:
KUBECONFORM_VERSION: v0.7.0
KUBECTL_VERSION: v1.36.3
KUBERNETES_API: https://host.containers.internal:6443
KUBERNETES_TLS_SERVER_NAME: 192.168.0.2
NAMESPACE: coturn
jobs:
validate:
name: Validate manifests
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Validate scripts and Kubernetes resources
run: |
sh -n install.sh bootstrap-turn-secret.sh
bash -n create-ci-kubeconfig.sh
docker run --rm --interactive \
"ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c" \
-strict -summary -ignore-missing-schemas < coturn.yaml
docker run --rm --interactive \
"ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c" \
-strict -summary < coturn-relay-service.yaml
docker run --rm --interactive \
"ghcr.io/yannh/kubeconform:${KUBECONFORM_VERSION}@sha256:85dbef6b4b312b99133decc9c6fc9495e9fc5f92293d4ff3b7e1b30f5611823c" \
-strict -summary < ci-deployer.yaml
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
run: |
export KUBECONFIG="${RUNNER_TEMP}/kubeconfig"
"${RUNNER_TEMP}/kubectl" apply \
--dry-run=server \
--validate=false \
--filename coturn.yaml \
--filename coturn-relay-service.yaml
"${RUNNER_TEMP}/kubectl" apply \
--validate=false \
--filename coturn.yaml \
--filename coturn-relay-service.yaml
"${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \
rollout status deployment/coturn --timeout=5m
"${RUNNER_TEMP}/kubectl" --namespace "${NAMESPACE}" \
wait --for=condition=Ready certificate/coturn --timeout=5m
+5
View File
@@ -0,0 +1,5 @@
# Local secret/bootstrap material must never be committed.
.env
turn-secret.yaml
kubeconfig
*.kubeconfig
+38 -2
View File
@@ -1,3 +1,39 @@
# coturn-kubernetes
# coturn
Configuration files to run my coturn server for voip in matrix/synapse in my kubernetes cluster with cert-manager and L2 LoadBalancer
Authenticated TURN for Synapse, exposed through MetalLB at `192.168.0.19`.
## Local deployment
The TURN shared secret is stored in Kubernetes as `turn-shared-secret` in the
`coturn` and `matrix` namespaces. It is never written into a committed file.
Bootstrap the existing secret once, then deploy:
```sh
TURN_SHARED_SECRET='<64-character-secret>' ./bootstrap-turn-secret.sh
./install.sh
```
## Gitea Actions CD
Bootstrap the restricted deployer and generate its kubeconfig:
```sh
./create-ci-kubeconfig.sh
```
When the Kubernetes API is reachable only through `dahoam`, use:
```sh
KUBECTL_SSH_HOST=dahoam \
SSH_CONFIG="$HOME/.ssh/config" \
./create-ci-kubeconfig.sh
```
Create one repository Actions secret named `KUBE_CONFIG_BASE64` containing the
single base64 line printed by that command. The deployer can update only the
existing coturn Services, ServiceAccount, Deployment, and Certificate. It
cannot read Kubernetes Secrets or modify RBAC.
No TURN secret is required in Gitea. The committed Deployment references the
existing Kubernetes Secret directly.
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
set -eu
SECRET_NAME=turn-shared-secret
if [ -z "${TURN_SHARED_SECRET:-}" ]; then
echo "Set TURN_SHARED_SECRET to the existing 64-character shared secret." >&2
exit 1
fi
case "$TURN_SHARED_SECRET" in
*[!0-9a-f]*|'')
echo "TURN_SHARED_SECRET must contain lowercase hexadecimal characters only." >&2
exit 1
;;
esac
if [ "${#TURN_SHARED_SECRET}" -ne 64 ]; then
echo "TURN_SHARED_SECRET must be exactly 64 characters." >&2
exit 1
fi
for namespace in coturn matrix; do
kubectl create namespace "$namespace" --dry-run=client --output=yaml \
| kubectl apply --filename=-
kubectl create secret generic "$SECRET_NAME" \
--namespace "$namespace" \
--from-literal="shared-secret=$TURN_SHARED_SECRET" \
--dry-run=client --output=yaml \
| kubectl apply --filename=-
done
echo "Stored the existing TURN shared secret in coturn/$SECRET_NAME and matrix/$SECRET_NAME."
+82
View File
@@ -0,0 +1,82 @@
# Bootstrap once with an administrator identity. CI can update only the
# existing coturn resources and cannot read Secrets or change RBAC.
apiVersion: v1
kind: ServiceAccount
metadata:
name: coturn-deployer
namespace: coturn
automountServiceAccountToken: false
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: coturn-deployer
namespace: coturn
rules:
- apiGroups:
- ""
resources:
- serviceaccounts
resourceNames:
- coturn
verbs:
- get
- patch
- update
- apiGroups:
- ""
resources:
- services
resourceNames:
- coturn-tcp
- coturn-udp
- coturn-relay
verbs:
- get
- patch
- update
- apiGroups:
- apps
resources:
- deployments
resourceNames:
- coturn
verbs:
- get
- patch
- update
- watch
- apiGroups:
- cert-manager.io
resources:
- certificates
resourceNames:
- coturn
verbs:
- get
- patch
- update
- watch
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: coturn-deployer
namespace: coturn
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: coturn-deployer
subjects:
- kind: ServiceAccount
name: coturn-deployer
namespace: coturn
---
apiVersion: v1
kind: Secret
metadata:
name: coturn-deployer-token
namespace: coturn
annotations:
kubernetes.io/service-account.name: coturn-deployer
type: kubernetes.io/service-account-token
+56
View File
@@ -0,0 +1,56 @@
apiVersion: v1
kind: Service
metadata:
name: coturn-relay
namespace: coturn
annotations:
metallb.io/loadBalancerIPs: "192.168.0.19"
metallb.io/allow-shared-ip: coturn
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app.kubernetes.io/instance: coturn
app.kubernetes.io/name: coturn
ports:
- {name: relay-49160, port: 49160, targetPort: 49160, protocol: UDP}
- {name: relay-49161, port: 49161, targetPort: 49161, protocol: UDP}
- {name: relay-49162, port: 49162, targetPort: 49162, protocol: UDP}
- {name: relay-49163, port: 49163, targetPort: 49163, protocol: UDP}
- {name: relay-49164, port: 49164, targetPort: 49164, protocol: UDP}
- {name: relay-49165, port: 49165, targetPort: 49165, protocol: UDP}
- {name: relay-49166, port: 49166, targetPort: 49166, protocol: UDP}
- {name: relay-49167, port: 49167, targetPort: 49167, protocol: UDP}
- {name: relay-49168, port: 49168, targetPort: 49168, protocol: UDP}
- {name: relay-49169, port: 49169, targetPort: 49169, protocol: UDP}
- {name: relay-49170, port: 49170, targetPort: 49170, protocol: UDP}
- {name: relay-49171, port: 49171, targetPort: 49171, protocol: UDP}
- {name: relay-49172, port: 49172, targetPort: 49172, protocol: UDP}
- {name: relay-49173, port: 49173, targetPort: 49173, protocol: UDP}
- {name: relay-49174, port: 49174, targetPort: 49174, protocol: UDP}
- {name: relay-49175, port: 49175, targetPort: 49175, protocol: UDP}
- {name: relay-49176, port: 49176, targetPort: 49176, protocol: UDP}
- {name: relay-49177, port: 49177, targetPort: 49177, protocol: UDP}
- {name: relay-49178, port: 49178, targetPort: 49178, protocol: UDP}
- {name: relay-49179, port: 49179, targetPort: 49179, protocol: UDP}
- {name: relay-49180, port: 49180, targetPort: 49180, protocol: UDP}
- {name: relay-49181, port: 49181, targetPort: 49181, protocol: UDP}
- {name: relay-49182, port: 49182, targetPort: 49182, protocol: UDP}
- {name: relay-49183, port: 49183, targetPort: 49183, protocol: UDP}
- {name: relay-49184, port: 49184, targetPort: 49184, protocol: UDP}
- {name: relay-49185, port: 49185, targetPort: 49185, protocol: UDP}
- {name: relay-49186, port: 49186, targetPort: 49186, protocol: UDP}
- {name: relay-49187, port: 49187, targetPort: 49187, protocol: UDP}
- {name: relay-49188, port: 49188, targetPort: 49188, protocol: UDP}
- {name: relay-49189, port: 49189, targetPort: 49189, protocol: UDP}
- {name: relay-49190, port: 49190, targetPort: 49190, protocol: UDP}
- {name: relay-49191, port: 49191, targetPort: 49191, protocol: UDP}
- {name: relay-49192, port: 49192, targetPort: 49192, protocol: UDP}
- {name: relay-49193, port: 49193, targetPort: 49193, protocol: UDP}
- {name: relay-49194, port: 49194, targetPort: 49194, protocol: UDP}
- {name: relay-49195, port: 49195, targetPort: 49195, protocol: UDP}
- {name: relay-49196, port: 49196, targetPort: 49196, protocol: UDP}
- {name: relay-49197, port: 49197, targetPort: 49197, protocol: UDP}
- {name: relay-49198, port: 49198, targetPort: 49198, protocol: UDP}
- {name: relay-49199, port: 49199, targetPort: 49199, protocol: UDP}
- {name: relay-49200, port: 49200, targetPort: 49200, protocol: UDP}
+193
View File
@@ -0,0 +1,193 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: coturn
namespace: coturn
labels:
app.kubernetes.io/name: coturn
app.kubernetes.io/instance: coturn
automountServiceAccountToken: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: coturn
namespace: coturn
labels:
app.kubernetes.io/name: coturn
app.kubernetes.io/instance: coturn
app.kubernetes.io/version: 4.10.0-r1
spec:
replicas: 1
revisionHistoryLimit: 3
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: coturn
app.kubernetes.io/instance: coturn
template:
metadata:
labels:
app.kubernetes.io/name: coturn
app.kubernetes.io/instance: coturn
spec:
serviceAccountName: coturn
automountServiceAccountToken: false
containers:
- name: coturn
image: coturn/coturn:4.10.0-r1
imagePullPolicy: IfNotPresent
args:
- --listening-port=3478
- --tls-listening-port=5349
- --fingerprint
- --cert=/etc/ssl/certs/cert.pem
- --pkey=/etc/ssl/private/privkey.pem
- --realm=brunner.ninja
- --log-file=stdout
- --no-software-attribute
- --no-cli
- --use-auth-secret
- --static-auth-secret=$(TURN_SHARED_SECRET)
- --external-ip=37.186.4.16
- --min-port=49160
- --max-port=49200
- --no-multicast-peers
env:
- name: TURN_SHARED_SECRET
valueFrom:
secretKeyRef:
name: turn-shared-secret
key: shared-secret
ports:
- name: tcp
containerPort: 3478
protocol: TCP
- name: udp
containerPort: 3478
protocol: UDP
- name: tcp-tls
containerPort: 5349
protocol: TCP
- name: udp-tls
containerPort: 5349
protocol: UDP
readinessProbe:
tcpSocket:
port: tcp
livenessProbe:
tcpSocket:
port: tcp
initialDelaySeconds: 30
resources:
requests:
cpu: 100m
memory: 50Mi
limits:
cpu: 100m
memory: 50Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
add:
- NET_BIND_SERVICE
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65534
volumeMounts:
- name: certs
mountPath: /etc/ssl/certs
readOnly: true
- name: private
mountPath: /etc/ssl/private
readOnly: true
volumes:
- name: certs
secret:
secretName: coturn
items:
- key: tls.crt
path: cert.pem
- name: private
secret:
secretName: coturn
items:
- key: tls.key
path: privkey.pem
---
apiVersion: v1
kind: Service
metadata:
name: coturn-tcp
namespace: coturn
labels:
app.kubernetes.io/name: coturn
app.kubernetes.io/instance: coturn
annotations:
metallb.io/loadBalancerIPs: "192.168.0.19"
metallb.io/allow-shared-ip: coturn
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app.kubernetes.io/name: coturn
app.kubernetes.io/instance: coturn
ports:
- name: tcp
port: 3478
targetPort: tcp
protocol: TCP
- name: tcp-tls
port: 5349
targetPort: tcp-tls
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: coturn-udp
namespace: coturn
labels:
app.kubernetes.io/name: coturn
app.kubernetes.io/instance: coturn
annotations:
metallb.io/loadBalancerIPs: "192.168.0.19"
metallb.io/allow-shared-ip: coturn
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app.kubernetes.io/name: coturn
app.kubernetes.io/instance: coturn
ports:
- name: udp
port: 3478
targetPort: udp
protocol: UDP
- name: udp-tls
port: 5349
targetPort: udp-tls
protocol: UDP
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: coturn
namespace: coturn
labels:
app.kubernetes.io/name: coturn
app.kubernetes.io/instance: coturn
spec:
secretName: coturn
dnsNames:
- brunner.ninja
issuerRef:
group: cert-manager.io
kind: ClusterIssuer
name: letsencrypt-dns
usages:
- digital signature
- key encipherment
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env bash
set -euo pipefail
project_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
namespace=${NAMESPACE:-coturn}
service_account=${SERVICE_ACCOUNT:-coturn-deployer}
secret=${TOKEN_SECRET:-coturn-deployer-token}
ssh_host=${KUBECTL_SSH_HOST:-}
ssh_config=${SSH_CONFIG:-}
cluster_kubectl() {
if [[ -n "${ssh_host}" ]]; then
if [[ -n "${ssh_config}" ]]; then
ssh -F "${ssh_config}" "${ssh_host}" kubectl "$@"
else
ssh "${ssh_host}" kubectl "$@"
fi
else
kubectl "$@"
fi
}
if [[ -n "${ssh_host}" ]]; then
cluster_kubectl apply --filename=- < "${project_dir}/ci-deployer.yaml" >&2
else
cluster_kubectl apply --filename "${project_dir}/ci-deployer.yaml" >&2
fi
for attempt in {1..30}; do
if cluster_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=$(cluster_kubectl config view --minify --output=jsonpath='{.clusters[0].cluster.server}')
cluster_kubectl --namespace "${namespace}" get secret "${secret}" \
--output=jsonpath='{.data.ca\.crt}' | base64 --decode > "${workdir}/ca.crt"
token=$(cluster_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 coturn \
--cluster=cluster \
--user="${service_account}" \
--namespace="${namespace}" >/dev/null
kubectl config use-context coturn >/dev/null
echo "Store the following 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
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
set -eu
NAMESPACE=coturn
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
if ! command -v kubectl >/dev/null 2>&1; then
echo "Required command not found: kubectl" >&2
exit 1
fi
kubectl apply --dry-run=server --validate=false \
--filename "$SCRIPT_DIR/coturn.yaml" \
--filename "$SCRIPT_DIR/coturn-relay-service.yaml"
kubectl apply --validate=false \
--filename "$SCRIPT_DIR/coturn.yaml" \
--filename "$SCRIPT_DIR/coturn-relay-service.yaml"
kubectl wait --for=condition=Ready certificate/coturn \
--namespace "$NAMESPACE" --timeout=10m
kubectl rollout status deployment/coturn \
--namespace "$NAMESPACE" --timeout=5m
echo
echo "coturn is available at brunner.ninja on 3478 and 5349."
echo "MetalLB: 192.168.0.19; UDP relay range: 49160-49200."