All Products
Search
Document Center

Container Service for Kubernetes:Best practices for using Arena in a multi-user scenario

Last Updated:Mar 26, 2026

This topic walks through a complete setup for running Arena in a shared GPU cluster with multiple internal teams. It covers user isolation, role-based access control (RBAC), resource quotas, and shared storage.

This setup uses namespace-based soft isolation and is suited for trusted tenants — internal teams or departments sharing a single ACK cluster. If you need cluster-level isolation between external organizations, use separate ACK clusters instead.

How this setup works

When multiple developers share a GPU cluster, you need a way to divide them into user groups, enforce per-group resource limits, and control job visibility. This setup maps three layers together:

  • Linux user groupKubernetes namespaceArena user group

Each layer enforces a different aspect of isolation. The Linux layer controls who can log in and which environment they run in. The namespace layer isolates Kubernetes resources. The Arena layer ensures each user sees only their own jobs.

This setup covers the following five tasks:

  • Task 1: Create user groups dev1 and dev2, and add Bob to dev1 and Tom to dev2.

  • Task 2: Allow Bob and Tom to log in to the client only with their own accounts, each in a separate Arena environment.

  • Task 3: Grant Bob and Tom permission to view and manage only the jobs they submit.

  • Task 4: Allocate GPU, CPU, and memory resources to each user group.

  • Task 5: Create shared volumes accessible within a group only, and shared volumes accessible across groups.

Resource allocation

User group User GPU CPU Memory Shared volumes
dev1 Bob 1 Unlimited Unlimited dev1-public and department1-public-dev1
dev2 Tom 2 8 cores 60 GiB dev2-public and department1-public-dev2

department1-public-dev1 and department1-public-dev2 are mounted to the same directory on the Apsara File Storage NAS (NAS) file system, so users in both groups can access that data. dev1-public and dev2-public are mounted to separate directories and are accessible only to Bob and Tom respectively.

Example cluster

Hostname Role IP address GPUs CPU cores Memory
client01 Client 10.0.0.97 (private), 39.98.xxx.xxx (public) 0 2 8 GiB
master01 Master 10.0.0.91 (private) 0 4 8 GiB
master02 Master 10.0.0.92 (private) 0 4 8 GiB
master03 Master 10.0.0.93 (private) 0 4 8 GiB
worker01 Worker 10.0.0.94 (private) 1 4 30 GiB
worker02 Worker 10.0.0.95 (private) 1 4 30 GiB
worker03 Worker 10.0.0.96 (private) 1 4 30 GiB
All operations in this topic are performed using an administrator account on the client, unless otherwise stated.

Prerequisites

Before you begin, ensure that you have:

Step 1: Create users and user groups

Do not install or run Arena directly on a master node. Use the ECS client instead, and connect it to the ACK cluster via a kubeconfig file.

Create Linux users and groups

Create user IDs (UIDs) and group IDs (GIDs) for Bob and Tom on the client. The Linux account system enforces Task 2: each user can only log in with their own credentials and runs Arena in their own environment.

# Create Linux groups: dev1 and dev2.
groupadd -g 10001 dev1
groupadd -g 10002 dev2

# Create Linux users: Bob and Tom.
adduser -u 20001 -s /bin/bash -G dev1 -m bob
adduser -u 20002 -s /bin/bash -G dev2 -m tom

# Set passwords.
passwd bob
passwd tom

Create Kubernetes namespaces and service accounts

After jobs are submitted, they run in the ACK cluster. Each Linux user group maps to a Kubernetes namespace, and each Linux user maps to a service account within that namespace.

Log in to the client as the root user and run the following commands. For prerequisites on connecting kubectl to the ACK cluster, see Obtain the kubeconfig file of a cluster and use kubectl to connect to the cluster.

kubectl version 1.10 or later is required.
# Create namespaces for dev1 and dev2.
kubectl create namespace dev1
kubectl create namespace dev2

# Create service accounts for Bob and Tom.
kubectl create serviceaccount bob -n dev1
kubectl create serviceaccount tom -n dev2

Expected output:

namespace/dev1 created
namespace/dev2 created
serviceaccount/bob created
serviceaccount/tom created

Step 2: Configure Arena for each user

Install Arena

Install Arena once on the client. Log in as the root user, download the latest community release package, decompress it, and run install.sh. For more information, see Configure the Arena client.

Generate kubeconfig files

Each user needs their own kubeconfig file to access the ACK cluster with a scoped identity. Create a script named generate-kubeconfig.sh with the following content:

#!/usr/bin/env bash

set -e

NAMESPACE=
SERVICE_ACCOUNT=
DURATION=
OUTPUT=

help() {
    echo "Usage: $0 -n <namespace> -s <service-account> -d <duration> -o <output-file>"
    echo ""
    echo "Options:"
    echo "-n, --namespace <namespace>      Namespace of the service account."
    echo "-s, --service-account <name>     Name of the service account."
    echo "-d, --duration <duration>        Duration of the token e.g. 30d."
    echo "-o, --output <file>              Output file name. If not set, a temporary file will be created."
}

parse() {
    while [ $# -gt 0 ]; do
        case $1 in
        -n | --namespace)
            NAMESPACE="$2"
            shift 2
            ;;
        -s | --service-account)
            SERVICE_ACCOUNT="$2"
            shift 2
            ;;
        -d | --duration)
            DURATION="$2"
            shift 2
            ;;
        -o | --output)
            OUTPUT="$2"
            shift 2
            ;;
        *)
            help
            exit 0
            ;;
        esac
    done

    if [ -z "${NAMESPACE}" ] || [ -z "${SERVICE_ACCOUNT}" ] || [ -z "${DURATION}" ]; then
        help
        exit 0
    fi

    if [ -z "${OUTPUT}" ]; then
        OUTPUT=$(mktemp -d)/config
    elif [ -f "${OUTPUT}" ]; then
        echo "Output file \"${OUTPUT}\" already exists."
        exit 1
    fi
}

# Generate kubeconfig
generate_kubeconfig() {
    CONTEXT=$(kubectl config current-context)
    CLUSTER=$(kubectl config view -o jsonpath="{.contexts[?(@.name==\"${CONTEXT}\")].context.cluster}")
    SERVER=$(kubectl config view -o jsonpath="{.clusters[?(@.name==\"${CLUSTER}\")].cluster.server}")
    TOKEN=$(kubectl create token "${SERVICE_ACCOUNT}" --namespace "${NAMESPACE}" --duration="${DURATION}")
    CERT=$(mktemp)

    mkdir -p "$(dirname "${OUTPUT}")"
    kubectl config view --raw=true -o jsonpath="{.clusters[?(@.name==\"${CLUSTER}\")].cluster.certificate-authority-data}" | base64 -d >"${CERT}"
    kubectl config set-cluster "${CLUSTER}" --kubeconfig="${OUTPUT}" --server="${SERVER}" --embed-certs=true --certificate-authority="${CERT}" >/dev/null
    kubectl config set-credentials "${SERVICE_ACCOUNT}" --kubeconfig="${OUTPUT}" --token="${TOKEN}" >/dev/null
    kubectl config set-context "${CLUSTER}-${NAMESPACE}-${SERVICE_ACCOUNT}-context" --kubeconfig="${OUTPUT}" --cluster="${CLUSTER}" --user="${SERVICE_ACCOUNT}" --namespace="${NAMESPACE}" >/dev/null
    kubectl config use-context "${CLUSTER}-${NAMESPACE}-${SERVICE_ACCOUNT}-context" --kubeconfig="${OUTPUT}" >/dev/null
    rm "${CERT}"
    echo "Saved kubeconfig to \"${OUTPUT}\"."
}

main() {
    parse "$@"
    generate_kubeconfig
}

main "$@"

Generate kubeconfig files for Bob and Tom. The following commands set the token expiration to 720 hours — adjust this value based on your security policy.

bash generate-kubeconfig.sh -n dev1 -s bob -d 720h -o /home/bob/.kube/config
bash generate-kubeconfig.sh -n dev2 -s tom -d 720h -o /home/tom/.kube/config

Expected output:

Saved kubeconfig to "/home/bob/.kube/config".
Saved kubeconfig to "/home/tom/.kube/config".

After this step, Tasks 1 and 2 are complete. Bob and Tom can each log in to the client and run Arena in their own isolated environment.

Step 3: Configure RBAC permissions

Create roles

Create roles in the dev1 and dev2 namespaces. Bob and Tom are granted the minimum permissions needed to view and manage only their own jobs (Task 3).

The following table summarizes the permissions granted to each user:

Resource type Bob (dev1) Tom (dev2)
pods, pods/log, services Full access Full access
deployments, ReplicaSets Full access Full access
ConfigMaps Full access Full access
kubeflow.org resources Full access Full access
batch jobs Full access Full access
persistentvolumeclaims, events, services/proxy Read only Read only
Cluster-wide: pods, nodes, services, persistentvolumes Read only Read only

Create dev1_roles.yaml for user group dev1:

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: arena-topnode
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - services
  - deployments
  - nodes
  - nodes/*
  - services/proxy
  - persistentvolumes
  verbs:
  - get
  - list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: arena
  namespace: dev1
rules:
  - apiGroups:
      - ""
    resources:
      - configmaps
    verbs:
      - '*'
  - apiGroups:
      - ""
    resources:
      - services/proxy
      - persistentvolumeclaims
      - events
    verbs:
      - get
      - list
  - apiGroups:
      - ""
    resources:
      - pods
      - pods/log
      - services
    verbs:
      - '*'
  - apiGroups:
      - ""
      - apps
      - extensions
    resources:
      - deployments
      - replicasets
    verbs:
      - '*'
  - apiGroups:
      - kubeflow.org
    resources:
      - '*'
    verbs:
      - '*'
  - apiGroups:
      - batch
    resources:
      - jobs
    verbs:
      - '*'

Create dev2_roles.yaml for user group dev2:

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: arena-topnode
rules:
- apiGroups:
  - ""
  resources:
  - pods
  - services
  - deployments
  - nodes
  - nodes/*
  - services/proxy
  - persistentvolumes
  verbs:
  - get
  - list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: arena
  namespace: dev2
rules:
- apiGroups:
  - ""
  resources:
  - configmaps
  verbs:
  - '*'
- apiGroups:
  - ""
  resources:
  - services/proxy
  - persistentvolumeclaims
  - events
  verbs:
  - get
  - list
- apiGroups:
  - ""
  resources:
  - pods
  - pods/log
  - services
  verbs:
  - '*'
- apiGroups:
  - ""
  - apps
  - extensions
  resources:
  - deployments
  - replicasets
  verbs:
  - '*'
- apiGroups:
  - kubeflow.org
  resources:
  - '*'
  verbs:
  - '*'
- apiGroups:
  - batch
  resources:
  - jobs
  verbs:
  - '*'

Apply both role definition files:

kubectl apply -f dev1_roles.yaml
kubectl apply -f dev2_roles.yaml

Expected output:

clusterrole.rbac.authorization.k8s.io/arena-topnode created
role.rbac.authorization.k8s.io/arena created

clusterrole.rbac.authorization.k8s.io/arena-topnode unchanged
role.rbac.authorization.k8s.io/arena created

Verify the roles were created:

kubectl get role -n dev1
kubectl get role -n dev2

Expected output:

NAME    CREATED AT
arena   2024-09-14T08:25:34Z

NAME    CREATED AT
arena   2024-09-14T08:25:39Z

Bind roles to users

Create bob_rolebindings.yaml:

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: bob-arena-topnode
  namespace: dev1
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: arena-topnode
subjects:
- kind: ServiceAccount
  name: bob
  namespace: dev1
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: bob-arena
  namespace: dev1
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: arena
subjects:
- kind: ServiceAccount
  name: bob
  namespace: dev1

Create tom_rolebindings.yaml:

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: tom-arena-topnode
  namespace: dev2
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: arena-topnode
subjects:
- kind: ServiceAccount
  name: tom
  namespace: dev2
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: tom-arena
  namespace: dev2
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: arena
subjects:
- kind: ServiceAccount
  name: tom
  namespace: dev2

Apply both files:

kubectl apply -f bob_rolebindings.yaml
kubectl apply -f tom_rolebindings.yaml

Expected output:

clusterrolebinding.rbac.authorization.k8s.io/bob-arena-topnode created
rolebinding.rbac.authorization.k8s.io/bob-arena created

clusterrolebinding.rbac.authorization.k8s.io/tom-arena-topnode created
rolebinding.rbac.authorization.k8s.io/tom-arena created

Verify the role bindings:

kubectl get rolebinding -n dev1
kubectl get rolebinding -n dev2

Expected output:

NAME        ROLE         AGE
bob-arena   Role/arena   34s

NAME        ROLE         AGE
tom-arena   Role/arena   33s

Tasks 1–3 are now complete.

Step 4: Set resource quotas

Kubernetes ResourceQuota objects enforce per-namespace resource limits. When a submitted job requests more resources than the namespace quota allows, the ACK cluster rejects the job immediately. This prevents one user group from consuming resources allocated to another.

The quotas in this example reflect the resource allocation in the Resource allocation table:

  • dev1: 1 GPU, unlimited CPU and memory. The business policy for dev1 allows unrestricted CPU and memory usage, so only the GPU quota is the binding constraint for Bob.

  • dev2: 2 GPUs, 8 CPU cores, and 60 GiB of memory. The business policy for dev2 enforces strict resource bounds. Tom must explicitly request CPU and memory when submitting jobs, or the ACK cluster rejects the request. A quota of 2 GPUs means dev2 can run at most two GPU-requiring jobs simultaneously.

Create dev1_quota.yaml:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev1-compute-resources
  namespace: dev1
spec:
  hard:
    requests.cpu: "10"
    requests.memory: 10Gi
    limits.cpu: "15"
    limits.memory: 20Gi
    requests.nvidia.com/gpu: 2

Create dev2_quota.yaml:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: dev2-compute-resources
  namespace: dev2
spec:
  hard:
    requests.nvidia.com/gpu: 2

Apply both quota files:

kubectl apply -f dev1_quota.yaml
kubectl apply -f dev2_quota.yaml

Verify that the quotas are applied and check current usage:

# Query quotas in dev1 and dev2.
kubectl get resourcequotas -n dev1
kubectl get resourcequotas -n dev2

# Query detailed usage.
kubectl describe resourcequotas dev1-compute-resources -n dev1
kubectl describe resourcequotas dev2-compute-resources -n dev2

Expected output:

NAME                     AGE   REQUEST                                                                     LIMIT
dev1-compute-resources   9s    requests.cpu: 0/10, requests.memory: 0/10Gi, requests.nvidia.com/gpu: 0/2   limits.cpu: 0/15, limits.memory: 0/20Gi

NAME                     AGE   REQUEST                        LIMIT
dev2-compute-resources   10s   requests.nvidia.com/gpu: 0/2

Name:                    dev1-compute-resources
Namespace:               dev1
Resource                 Used  Hard
--------                 ----  ----
limits.cpu               0     15
limits.memory            0     20Gi
requests.cpu             0     10
requests.memory          0     10Gi
requests.nvidia.com/gpu  0     2

Name:                    dev2-compute-resources
Namespace:               dev2
Resource                 Used  Hard
--------                 ----  ----
requests.nvidia.com/gpu  0     2

Task 4 is now complete.

Step 5: Create NAS volumes for shared storage

Create two types of shared volumes:

  • Group-private volumes: accessible only to users within a specific group (dev1-public for Bob, dev2-public for Tom)

  • Cross-group volumes: accessible to users in both groups (department1-public-dev1 and department1-public-dev2, which are mounted to the same NAS directory)

Create a NAS file system

Log in to the NAS console, create a NAS file system, and add a mount target. For more information, see Configure a shared NAS volume.Alibaba Cloud NAS console

Create persistent volumes and claims

  1. Create four persistent volumes (PVs) — dev1-public, dev2-public, department1-public-dev1, and department1-public-dev2. Mount department1-public-dev1 and department1-public-dev2 to the same NAS directory so both groups can access the shared data. Mount dev1-public and dev2-public to separate directories. For more information, see Mount a statically provisioned NAS volume.

    Select the mount target you added in the previous step.

    PV

  2. Create one persistent volume claim (PVC) for each PV. For more information, see Mount a statically provisioned NAS volume. After the PVCs are created, department1-public-dev1 and dev1-public appear in namespace dev1, and department1-public-dev2 and dev2-public appear in namespace dev2.

Verify volume configuration

Log in to the client as the root user and run:

# Query volumes available to dev1.
arena data list -n dev1

# Query volumes available to dev2.
arena data list -n dev2

Expected output:

check

All five tasks are now complete.

Step 6: Run Arena as Bob and Tom

Bob's account

  1. Log in to the client as Bob and query available volumes:

    ssh bob@39.98.xxx.xx
    arena data list

    Expected output:

    result

  2. Submit a training job that requires one GPU:

    arena submit tf \
            --name=tf-git-bob-01 \
            --gpus=1 \
            --image=tensorflow/tensorflow:1.5.0-devel-gpu \
            --sync-mode=git \
            --sync-source=https://code.aliyun.com/xiaozhou/tensorflow-sample-code.git \
            "python code/tensorflow-sample-code/tfjob/docker/mnist/main.py --max_steps 10000 --data_dir=code/tensorflow-sample-code/data"
  3. List all jobs submitted by Bob:

    arena list

    Expected output:

    bob

  4. Submit a second job requesting one GPU:

    arena submit tf \
            --name=tf-git-bob-02 \
            --gpus=1 \
            --image=tensorflow/tensorflow:1.5.0-devel-gpu \
            --sync-mode=git \
            --sync-source=https://code.aliyun.com/xiaozhou/tensorflow-sample-code.git \
            "python code/tensorflow-sample-code/tfjob/docker/mnist/main.py --max_steps 10000 --data_dir=code/tensorflow-sample-code/data"

    dev1 has a quota of only 1 GPU, which the first job already occupies. The ACK cluster suspends the second job even though the cluster overall still has free GPUs. resultreason To confirm the quota is exhausted, run:

    kubectl describe resourcequotas dev1-compute-resources -n dev1

Tom's account

  1. Log in to the client as Tom and query available volumes:

    ssh tom@39.98.xx.xx
    arena data list

    Expected output:

    tom job

  2. List all jobs submitted by Tom:

    arena list

    Tom cannot see the jobs submitted by Bob — namespace isolation prevents cross-group job visibility.

    list

  3. Submit a training job. Because dev2 has CPU and memory quotas, specify both when submitting:

     arena submit tf \
             --name=tf-git-tom-01 \
             --gpus=1 \
             --chief-cpu=2 \
             --chief-memory=10Gi \
             --image=tensorflow/tensorflow:1.5.0-devel-gpu \
             --sync-mode=git \
             --sync-source=https://code.aliyun.com/xiaozhou/tensorflow-sample-code.git \
             "python code/tensorflow-sample-code/tfjob/docker/mnist/main.py --max_steps 10000 --data_dir=code/tensorflow-sample-code/data"
  4. Submit a second job:

     arena submit tf \
             --name=tf-git-tom-02 \
             --gpus=1 \
             --chief-cpu=2 \
             --chief-memory=10Gi \
             --image=tensorflow/tensorflow:1.5.0-devel-gpu \
             --sync-mode=git \
             --sync-source=https://code.aliyun.com/xiaozhou/tensorflow-sample-code.git \
             "python code/tensorflow-sample-code/tfjob/docker/mnist/main.py --max_steps 10000 --data_dir=code/tensorflow-sample-code/data"
  5. List all jobs submitted by Tom:

    arena list

    Expected output: Tom's dev2 group has a quota of 2 GPUs, so both jobs run.

    result

Results

The setup demonstrates end-to-end multi-tenant isolation with Arena:

  • Bob and Tom each log in with their own accounts and run Arena in separate environments.

  • Each user sees and manages only their own jobs.

  • Resource quotas prevent one user group from consuming resources allocated to another.

  • Shared volumes give each group access to both group-private data and cross-group shared data.