Examples
Examples
Real-world usage patterns for redroid-operator. Each example is self-contained and can be adapted to your environment.
Example 1 — Daily Automation
The canonical use case: run a game automation bot automatically every morning against one or more game accounts, each living in its own overlay partition.
Storage
# storage.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redroid-data-base-pvc
namespace: default
spec:
accessModes: [ReadWriteMany]
resources:
requests:
storage: 15Gi # shared read-only base layer (game APK + common data)
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: redroid-data-diff-pvc
namespace: default
spec:
accessModes: [ReadWriteMany]
resources:
requests:
storage: 15Gi # per-instance writable overlay (account state diverges here)
Instances
Each account gets its own RedroidInstance at a unique spec.index. They share redroid-data-base-pvc as a read-only lower layer.
# redroid-instances.yaml
apiVersion: redroid.isning.moe/v1alpha1
kind: RedroidInstance
metadata:
name: android-0
namespace: default
spec:
index: 0
image: redroid/redroid:16.0.0-latest
sharedDataPVC: redroid-data-base-pvc
diffDataPVC: redroid-data-diff-pvc
gpuMode: host
---
apiVersion: redroid.isning.moe/v1alpha1
kind: RedroidInstance
metadata:
name: android-1
namespace: default
spec:
index: 1
image: redroid/redroid:16.0.0-latest
sharedDataPVC: redroid-data-base-pvc
diffDataPVC: redroid-data-diff-pvc
gpuMode: host
suspend: false # set true to free resources when account is not in use
Task config
# task-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: task-config
namespace: default
data:
task-config.json: |
{
"tasks": [
{ "type": "StartUp" },
{ "type": "Main" },
{ "type": "Finish" }
]
}
Daily task (CronJob)
# daily-task.yaml
apiVersion: redroid.isning.moe/v1alpha1
kind: RedroidTask
metadata:
name: daily
namespace: default
spec:
schedule: "0 4 * * *" # 04:00 every day
timezone: "Asia/Shanghai"
instances:
- name: android-0
- name: android-1 # comment out if android-1's spec.suspend: true
integrations:
- name: bot
image: ghcr.io/example/game-bot:latest
imagePullPolicy: Always
command: ["game-bot"]
args: ["run", "--config", "/etc/bot/task-config.json"]
configs:
- configMapName: task-config
key: task-config.json
mountPath: /etc/bot/task-config.json
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
activeDeadlineSeconds: 7200 # abort if a run takes more than 2 hours
kubectl apply -f storage.yaml
kubectl apply -f redroid-instances.yaml
kubectl apply -f task-config.yaml
kubectl apply -f daily-task.yaml
# Watch the daily run
kubectl get redroidtasks -w
kubectl logs -l redroid.isning.moe/task=daily -c bot -f
Example 2 — On-Demand Wake (wakeInstance)
Run a task against an instance that is normally kept suspended (spec.suspend: true) — e.g. a second account that you only want to run on manual trigger.
wakeInstance: true tells the task controller to:
- Set
status.wokenon the instance → instance controller starts the Pod. - Wait for
phase == Running. - Execute the Job.
- Clear
status.woken→ instance controller stops the Pod again.
The spec.suspend field is never modified, so GitOps tools (Flux, Argo CD) see no drift.
Instance (normally off)
apiVersion: redroid.isning.moe/v1alpha1
kind: RedroidInstance
metadata:
name: android-1
namespace: default
spec:
index: 1
image: redroid/redroid:16.0.0-latest
sharedDataPVC: redroid-data-base-pvc
diffDataPVC: redroid-data-diff-pvc
gpuMode: host
suspend: true # Pod is normally stopped; wakeInstance starts it temporarily
One-shot wake task
# wakeinstance-task.yaml
apiVersion: redroid.isning.moe/v1alpha1
kind: RedroidTask
metadata:
name: wake-run
namespace: default
spec:
# No schedule = one-shot Job (delete-and-recreate to re-run).
wakeInstance: true # powers on instances with spec.suspend: true while Job runs
instances:
- name: android-1
integrations:
- name: bot
image: ghcr.io/example/game-bot:latest
imagePullPolicy: Always
command: ["game-bot"]
args: ["run", "--config", "/etc/bot/task-config.json"]
configs:
- configMapName: task-config
key: task-config.json
mountPath: /etc/bot/task-config.json
ttlSecondsAfterFinished: 3600 # auto-clean Job 1 hour after completion
# Trigger an on-demand run:
kubectl apply -f wakeinstance-task.yaml
# Watch execution:
kubectl get redroidinstances android-1 -w # observe Stopped → Running → Stopped
# Or re-trigger by deleting and re-applying:
kubectl delete redroidtask wake-run
kubectl apply -f wakeinstance-task.yaml
Note:
wakeInstanceis only meaningful for one-shot tasks (nospec.schedule). For scheduled tasks, instances should be running continuously or managed separately.
Example 3 — Overlayfs-Safe Storage Access (suspendInstance)
Some tasks (base-layer update, device image backup) need exclusive write access to the overlayfs storage. Running them while normal instances have the PVC mounted read-only risks corruption.
suspendInstance: true tells the controller to:
- Set
status.suspendedon the instance → Pod is stopped. - Wait for
phase == Stopped. - Execute the Job (now has exclusive storage access).
- Clear
status.suspended→ Pod restarts automatically.
# backup-task.yaml
apiVersion: redroid.isning.moe/v1alpha1
kind: RedroidTask
metadata:
name: diff-backup
namespace: default
spec:
suspendInstance: true # stops instance Pod before Job runs, restarts after
instances:
- name: android-0
integrations:
- name: backup
image: busybox:latest
command: [sh, -c]
args:
- |
echo "Instance stopped. Safe to access /data-diff."
tar czf /backup/android-0-$(date +%F).tar.gz /data-diff/0
volumeMounts:
- name: backup-vol
mountPath: /backup
- name: diff-vol
mountPath: /data-diff
kubectl apply -f backup-task.yaml
kubectl logs -l redroid.isning.moe/task=diff-backup -c backup -f
Example 4 — Base Layer Initialisation
All normal instances share a common read-only base layer (redroid-data-base-pvc). Use a base-mode instance to write the initial state (Android setup, APK installs, account login).
Warning: Never run base-mode and normal instances against the same PVC concurrently.
One-time setup (manual)
# base-init.yaml
apiVersion: redroid.isning.moe/v1alpha1
kind: RedroidInstance
metadata:
name: android-base
namespace: default
spec:
index: 255 # high index avoids conflicts with normal instances
image: redroid/redroid:16.0.0-latest
sharedDataPVC: redroid-data-base-pvc
diffDataPVC: redroid-data-diff-pvc # required by schema; unused in baseMode
baseMode: true # mounts sharedDataPVC directly as /data (read-write)
gpuMode: host
suspend: false
kubectl apply -f base-init.yaml
# Wait for Running
kubectl get redroidinstances android-base -w
# Connect via ADB (requires kubectl-redroid plugin)
kubectl redroid instance port-forward android-base
# or manually:
kubectl port-forward svc/android-base 5555:5555 &
adb connect localhost:5555
# Install the game APK, log in, complete first-boot, etc.
adb install Game.apk
adb shell am start -n com.your.game/.MainActivity
# ... perform setup ...
# Suspend when done — all normal instances now inherit this state
kubectl patch redroidinstance android-base -p '{"spec":{"suspend":true}}'
Automated init via RedroidTask
# base-init-automated.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: base-init-script
namespace: default
data:
init.sh: |
#!/usr/bin/env sh
set -e
until adb connect "$ADB_ADDRESS"; do sleep 5; done
adb wait-for-device
sleep 30
adb install /apks/Game.apk
echo "[base-init] done"
---
apiVersion: redroid.isning.moe/v1alpha1
kind: RedroidTask
metadata:
name: base-init-task
namespace: default
spec:
instances:
- name: android-base
backoffLimit: 1
ttlSecondsAfterFinished: 3600
integrations:
- name: init
image: androidsdk/android-32:latest
command: ["/bin/sh"]
args: ["/scripts/init.sh"]
configs:
- configMapName: base-init-script
key: init.sh
mountPath: /scripts/init.sh
Example 5 — Multi-Instance with Parallelism Limit
Run the same task against five instances but only two at a time (useful when GPU or network bandwidth is limited).
apiVersion: redroid.isning.moe/v1alpha1
kind: RedroidTask
metadata:
name: weekly-scan
namespace: default
spec:
schedule: "0 3 * * 0" # every Sunday at 03:00
timezone: Asia/Tokyo
parallelism: 2 # at most 2 instance Jobs running simultaneously
instances:
- name: android-0
- name: android-1
- name: android-2
- name: android-3
- name: android-4
integrations:
- name: scanner
image: ghcr.io/myorg/malware-scanner:latest
args: ["--adb", "$(ADB_ADDRESS)"]
Example 6 — Expose ADB Externally
By default each instance has a ClusterIP Service. Override for external access:
NodePort
spec:
service:
type: NodePort
nodePort: 30555 # omit to auto-assign in the NodePort range
# Connect from outside the cluster
adb connect <node-ip>:30555
LoadBalancer (cloud)
spec:
service:
type: LoadBalancer
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: nlb
Example 7 — Secret-Based Proxy Configuration
Pass proxy credentials from a Kubernetes Secret into Android via androidboot.* args.
# proxy-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: proxy-creds
namespace: default
stringData:
host: "proxy.corp.example.com"
port: "8080"
user: "android"
pass: "s3cr3t"
# proxy-instance.yaml
apiVersion: redroid.isning.moe/v1alpha1
kind: RedroidInstance
metadata:
name: android-proxy
namespace: default
spec:
index: 0
image: redroid/redroid:16.0.0-latest
sharedDataPVC: redroid-data-base-pvc
diffDataPVC: redroid-data-diff-pvc
gpuMode: host
extraEnv:
- name: PROXY_HOST
valueFrom:
secretKeyRef:
name: proxy-creds
key: host
- name: PROXY_PORT
valueFrom:
secretKeyRef:
name: proxy-creds
key: port
extraArgs:
- "androidboot.redroid_net_proxy_type=static"
- "androidboot.redroid_net_proxy_host=$(PROXY_HOST)"
- "androidboot.redroid_net_proxy_port=$(PROXY_PORT)"
Example 8 — Per-Instance Volumes and Secrets
Supply instance-specific credentials (e.g. per-account API tokens stored in separate Secrets) using spec.instances[].volumes and spec.instances[].volumeMounts.
apiVersion: redroid.isning.moe/v1alpha1
kind: RedroidTask
metadata:
name: daily
namespace: default
spec:
schedule: "0 4 * * *"
# Task-level extra volume available to every instance.
volumes:
- name: shared-proxy-cert
configMap:
name: corp-proxy-ca
instances:
- name: android-0
# Per-instance Secret: only this instance's Job gets this volume.
volumes:
- name: account-token
secret:
secretName: android-0-token # Secret specific to account 0
volumeMounts:
- name: account-token
mountPath: /run/secrets/token
subPath: token
readOnly: true
- name: android-1
volumes:
- name: account-token
secret:
secretName: android-1-token # Different Secret for account 1
volumeMounts:
- name: account-token
mountPath: /run/secrets/token
subPath: token
readOnly: true
integrations:
- name: bot
image: ghcr.io/example/game-bot:latest
command: ["game-bot"]
args: ["run", "--token", "/run/secrets/token"]
# Integration-level mount present in every instance's container.
volumeMounts:
- name: shared-proxy-cert
mountPath: /etc/ssl/certs/corp-ca.crt
subPath: ca.crt
readOnly: true
configs:
- configMapName: task-config
key: task-config.json
mountPath: /etc/bot/task-config.json
Override rule: if an instance lists a volume with the same name as a task-level entry in spec.volumes, the instance definition wins. Reserved volumes (data-base, data-diff, dev-dri) and controller-generated ConfigMap volumes (cm-*) cannot be overridden by either task-level or instance-level volumes.
maa-gitops Reference
For a complete real-world example combining all the patterns above, see the maa-gitops repository.