Configure GitLab Runner with Kubernetes executor to automatically scale CI/CD workloads. Set up RBAC permissions, deploy pipelines to Kubernetes clusters, and implement resource management policies for efficient container orchestration.
Prerequisites
- Kubernetes cluster with admin access
- GitLab instance (self-hosted or GitLab.com)
- kubectl configured for cluster access
- Helm 3 installed
- Docker registry access
What this solves
GitLab CI/CD with Kubernetes runners automatically scales your build and deployment workloads based on demand. This setup eliminates the need to provision dedicated build servers and enables you to deploy applications directly to Kubernetes clusters using GitOps workflows.
Step-by-step configuration
Install kubectl and Helm
Install the Kubernetes command-line tool and Helm package manager to manage your cluster resources.
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt update && sudo apt install -y helm
Create GitLab Runner namespace and service account
Create a dedicated namespace for GitLab Runner with proper RBAC permissions to manage pods and services.
apiVersion: v1
kind: Namespace
metadata:
name: gitlab-runner
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitlab-runner
namespace: gitlab-runner
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: gitlab-runner
rules:
- apiGroups: [""]
resources: ["pods", "pods/exec", "pods/attach", "pods/log"]
verbs: ["get", "list", "watch", "create", "patch", "delete"]
- apiGroups: [""]
resources: ["secrets", "configmaps"]
verbs: ["get", "list", "watch", "create", "patch", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "create", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gitlab-runner
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: gitlab-runner
subjects:
- kind: ServiceAccount
name: gitlab-runner
namespace: gitlab-runner
kubectl apply -f /tmp/gitlab-runner-rbac.yaml
Get GitLab Runner registration token
Navigate to your GitLab project or group settings to obtain the runner registration token. Go to Settings > CI/CD > Runners and copy the registration token.
Install GitLab Runner with Helm
Deploy GitLab Runner to your Kubernetes cluster using the official Helm chart with Kubernetes executor configuration.
gitlabUrl: https://gitlab.example.com/
runnerToken: glrt-xxxxxxxxxxxxxxxxxx
runners:
config: |
[[runners]]
[runners.kubernetes]
namespace = "gitlab-runner"
image = "ubuntu:24.04"
privileged = false
service_account = "gitlab-runner"
# Resource requests and limits
cpu_request = "100m"
memory_request = "128Mi"
cpu_limit = "1000m"
memory_limit = "1Gi"
# Helper image configuration
helper_cpu_request = "50m"
helper_memory_request = "64Mi"
helper_cpu_limit = "100m"
helper_memory_limit = "128Mi"
# Pod security context
[runners.kubernetes.pod_security_context]
run_as_non_root = true
run_as_user = 1000
run_as_group = 1000
fs_group = 1000
# Node selector for specific worker nodes
[runners.kubernetes.node_selector]
"node-role.kubernetes.io/worker" = "true"
# Tolerations for dedicated CI nodes
[[runners.kubernetes.tolerations]]
key = "ci-workload"
operator = "Equal"
value = "true"
effect = "NoSchedule"
Resource limits for the runner manager pod
resources:
requests:
memory: 128Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 500m
Security context for runner manager
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
runAsNonRoot: true
runAsUser: 999
capabilities:
drop: ["ALL"]
serviceAccount:
create: false
name: gitlab-runner
helm repo add gitlab https://charts.gitlab.io
helm repo update
helm install gitlab-runner gitlab/gitlab-runner \
--namespace gitlab-runner \
--values /tmp/gitlab-runner-values.yaml
Configure resource quotas and limits
Set up resource quotas to prevent CI workloads from consuming all cluster resources and affecting other applications.
apiVersion: v1
kind: ResourceQuota
metadata:
name: gitlab-runner-quota
namespace: gitlab-runner
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "20"
persistentvolumeclaims: "10"
---
apiVersion: v1
kind: LimitRange
metadata:
name: gitlab-runner-limits
namespace: gitlab-runner
spec:
limits:
- default:
cpu: "1"
memory: 1Gi
defaultRequest:
cpu: 100m
memory: 128Mi
type: Container
- default:
storage: 1Gi
type: PersistentVolumeClaim
kubectl apply -f /tmp/gitlab-runner-quota.yaml
Create network policies for security
Implement network policies to restrict traffic between CI pods and other cluster workloads for enhanced security.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: gitlab-runner-netpol
namespace: gitlab-runner
spec:
podSelector:
matchLabels:
app: gitlab-runner
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: gitlab-runner
egress:
- to: []
ports:
- protocol: TCP
port: 80
- protocol: TCP
port: 443
- protocol: TCP
port: 53
- protocol: UDP
port: 53
- to:
- namespaceSelector:
matchLabels:
name: gitlab-runner
kubectl apply -f /tmp/gitlab-runner-netpol.yaml
Set up horizontal pod autoscaler
Configure HPA to automatically scale GitLab Runner pods based on CPU and memory usage during high CI workload periods.
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: gitlab-runner-hpa
namespace: gitlab-runner
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: gitlab-runner
minReplicas: 1
maxReplicas: 5
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleUp:
stabilizationWindowSeconds: 60
policies:
- type: Percent
value: 100
periodSeconds: 15
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 10
periodSeconds: 60
kubectl apply -f /tmp/gitlab-runner-hpa.yaml
Create GitLab CI pipeline for Kubernetes deployment
Set up a sample CI/CD pipeline that builds Docker images and deploys applications to your Kubernetes cluster.
variables:
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: ""
KUBECONFIG: /tmp/kubeconfig
stages:
- build
- test
- deploy
build-image:
stage: build
image: docker:24-dind
services:
- docker:24-dind
variables:
DOCKER_HOST: tcp://docker:2376
before_script:
- echo $CI_REGISTRY_PASSWORD | docker login -u $CI_REGISTRY_USER --password-stdin $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
rules:
- if: $CI_COMMIT_BRANCH == "main"
run-tests:
stage: test
image: ubuntu:24.04
script:
- apt-get update && apt-get install -y curl
- echo "Running application tests..."
- curl --version
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-staging:
stage: deploy
image: bitnami/kubectl:latest
before_script:
- echo $KUBECONFIG_CONTENT | base64 -d > $KUBECONFIG
- chmod 600 $KUBECONFIG
script:
- kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n staging
- kubectl rollout status deployment/myapp -n staging --timeout=300s
environment:
name: staging
url: https://staging.example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
deploy-production:
stage: deploy
image: bitnami/kubectl:latest
before_script:
- echo $KUBECONFIG_CONTENT | base64 -d > $KUBECONFIG
- chmod 600 $KUBECONFIG
script:
- kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -n production
- kubectl rollout status deployment/myapp -n production --timeout=600s
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == "main"
- when: manual
Configure pod disruption budget
Set up a pod disruption budget to ensure CI workloads remain available during cluster maintenance and node upgrades.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: gitlab-runner-pdb
namespace: gitlab-runner
spec:
minAvailable: 1
selector:
matchLabels:
app: gitlab-runner
kubectl apply -f /tmp/gitlab-runner-pdb.yaml
Verify your setup
Check that GitLab Runner is running and properly registered with your GitLab instance.
kubectl get pods -n gitlab-runner
kubectl logs -n gitlab-runner deployment/gitlab-runner
kubectl get hpa -n gitlab-runner
kubectl describe quota gitlab-runner-quota -n gitlab-runner
Verify the runner appears in your GitLab project settings under CI/CD > Runners. The status should show as "online" with the Kubernetes tag.
Configure scaling policies
Set up cluster autoscaler integration
Configure cluster autoscaler to automatically provision new nodes when CI workloads exceed current cluster capacity.
apiVersion: v1
kind: ConfigMap
metadata:
name: cluster-autoscaler-status
namespace: kube-system
data:
nodes.max: "10"
nodes.min: "2"
scale-down-delay-after-add: "10m"
scale-down-unneeded-time: "10m"
skip-nodes-with-local-storage: "false"
skip-nodes-with-system-pods: "false"
Configure priority classes
Create priority classes to ensure critical CI jobs get scheduled before lower-priority workloads during resource contention.
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: ci-high-priority
value: 1000
globalDefault: false
description: "High priority for critical CI/CD jobs"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: ci-low-priority
value: 100
globalDefault: false
description: "Low priority for background CI jobs"
kubectl apply -f /tmp/priority-classes.yaml
Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Runner shows offline in GitLab | Invalid registration token or network connectivity | Check token validity and firewall rules for GitLab access |
| Pods stuck in Pending state | Insufficient cluster resources or node selector constraints | Scale cluster or adjust resource requests in runner config |
| Permission denied errors | Insufficient RBAC permissions for service account | Verify ClusterRole includes required verbs for pod management |
| Build failures with "docker: command not found" | Missing Docker-in-Docker configuration | Use docker:24-dind service and set DOCKER_HOST variable |
| Network policy blocking builds | Overly restrictive egress rules | Allow egress to ports 80, 443, and DNS for package downloads |
Next steps
- Set up comprehensive monitoring for your Kubernetes cluster
- Automate GitLab backup processes with encryption
- Enhance security with advanced network policies
- Configure ingress controller for application routing
- Implement secure secrets management workflows
Running this in production?
Automated install script
Run this to automate the entire setup
#!/usr/bin/env bash
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables
GITLAB_URL=""
RUNNER_TOKEN=""
NAMESPACE="gitlab-runner"
CLEANUP_FILES=()
# Usage function
usage() {
echo "Usage: $0 --gitlab-url <url> --runner-token <token>"
echo "Example: $0 --gitlab-url https://gitlab.example.com --runner-token glrt-xxxxxxxxxx"
exit 1
}
# Logging functions
log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Cleanup function
cleanup() {
log_warning "Script interrupted or failed. Cleaning up..."
for file in "${CLEANUP_FILES[@]}"; do
[ -f "$file" ] && rm -f "$file"
done
}
# Trap for cleanup on error
trap cleanup ERR INT TERM
# Check if running as root or with sudo
check_privileges() {
if [[ $EUID -ne 0 ]]; then
log_error "This script must be run as root or with sudo"
exit 1
fi
}
# Detect distribution and package manager
detect_distro() {
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
PKG_MGR="apt"
PKG_INSTALL="apt install -y"
PKG_UPDATE="apt update"
;;
almalinux|rocky|centos|rhel|ol|fedora)
PKG_MGR="dnf"
PKG_INSTALL="dnf install -y"
PKG_UPDATE="dnf check-update || true"
;;
amzn)
PKG_MGR="yum"
PKG_INSTALL="yum install -y"
PKG_UPDATE="yum check-update || true"
;;
*)
log_error "Unsupported distribution: $ID"
exit 1
;;
esac
export PKG_MGR PKG_INSTALL PKG_UPDATE
log_info "Detected distribution: $PRETTY_NAME"
log_info "Using package manager: $PKG_MGR"
else
log_error "Cannot detect distribution. /etc/os-release not found."
exit 1
fi
}
# Install required packages
install_prerequisites() {
log_info "[1/7] Installing prerequisites..."
$PKG_UPDATE
case "$PKG_MGR" in
apt)
$PKG_INSTALL curl wget gnupg2 software-properties-common apt-transport-https ca-certificates
;;
dnf|yum)
$PKG_INSTALL curl wget gnupg2 which
;;
esac
log_success "Prerequisites installed"
}
# Install kubectl
install_kubectl() {
log_info "[2/7] Installing kubectl..."
# Download kubectl
local kubectl_version
kubectl_version=$(curl -L -s https://dl.k8s.io/release/stable.txt)
curl -LO "https://dl.k8s.io/release/${kubectl_version}/bin/linux/amd64/kubectl"
# Verify kubectl binary (optional but recommended)
curl -LO "https://dl.k8s.io/release/${kubectl_version}/bin/linux/amd64/kubectl.sha256"
if ! echo "$(cat kubectl.sha256) kubectl" | sha256sum --check; then
log_error "kubectl binary verification failed"
exit 1
fi
# Install kubectl
install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
# Cleanup
rm -f kubectl kubectl.sha256
# Verify installation
if kubectl version --client >/dev/null 2>&1; then
log_success "kubectl installed successfully"
else
log_error "kubectl installation failed"
exit 1
fi
}
# Install Helm
install_helm() {
log_info "[3/7] Installing Helm..."
case "$PKG_MGR" in
apt)
# Add Helm GPG key and repository
curl -fsSL https://baltocdn.com/helm/signing.asc | gpg --dearmor -o /usr/share/keyrings/helm.gpg
chmod 644 /usr/share/keyrings/helm.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" > /etc/apt/sources.list.d/helm-stable-debian.list
chmod 644 /etc/apt/sources.list.d/helm-stable-debian.list
apt update
$PKG_INSTALL helm
;;
dnf|yum)
# Use Helm installation script for RHEL-based systems
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh
rm -f get_helm.sh
;;
esac
# Verify installation
if helm version >/dev/null 2>&1; then
log_success "Helm installed successfully"
else
log_error "Helm installation failed"
exit 1
fi
}
# Create GitLab Runner RBAC configuration
create_rbac_config() {
log_info "[4/7] Creating GitLab Runner RBAC configuration..."
local rbac_file="/tmp/gitlab-runner-rbac.yaml"
CLEANUP_FILES+=("$rbac_file")
cat > "$rbac_file" << 'EOF'
apiVersion: v1
kind: Namespace
metadata:
name: gitlab-runner
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitlab-runner
namespace: gitlab-runner
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: gitlab-runner
rules:
- apiGroups: [""]
resources: ["pods", "pods/exec", "pods/attach", "pods/log"]
verbs: ["get", "list", "watch", "create", "patch", "delete"]
- apiGroups: [""]
resources: ["secrets", "configmaps"]
verbs: ["get", "list", "watch", "create", "patch", "delete"]
- apiGroups: [""]
resources: ["persistentvolumeclaims"]
verbs: ["get", "list", "watch", "create", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gitlab-runner
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: gitlab-runner
subjects:
- kind: ServiceAccount
name: gitlab-runner
namespace: gitlab-runner
EOF
chmod 644 "$rbac_file"
log_success "RBAC configuration created"
}
# Apply RBAC configuration
apply_rbac() {
log_info "[5/7] Applying RBAC configuration..."
if ! kubectl apply -f /tmp/gitlab-runner-rbac.yaml; then
log_error "Failed to apply RBAC configuration"
exit 1
fi
log_success "RBAC configuration applied"
}
# Create Helm values file
create_helm_values() {
log_info "[6/7] Creating Helm values configuration..."
local values_file="/tmp/gitlab-runner-values.yaml"
CLEANUP_FILES+=("$values_file")
cat > "$values_file" << EOF
gitlabUrl: ${GITLAB_URL}
runnerToken: ${RUNNER_TOKEN}
runners:
config: |
[[runners]]
[runners.kubernetes]
namespace = "${NAMESPACE}"
image = "ubuntu:24.04"
privileged = false
service_account = "gitlab-runner"
# Resource requests and limits
cpu_request = "100m"
memory_request = "128Mi"
cpu_limit = "1000m"
memory_limit = "1Gi"
# Helper image configuration
helper_cpu_request = "50m"
helper_memory_request = "64Mi"
helper_cpu_limit = "100m"
helper_memory_limit = "128Mi"
# Pod security context
[runners.kubernetes.pod_security_context]
run_as_non_root = true
run_as_user = 1000
run_as_group = 1000
fs_group = 1000
# Node selector for worker nodes
[runners.kubernetes.node_selector]
"node-role.kubernetes.io/worker" = "true"
rbac:
create: false
serviceAccountName: gitlab-runner
resources:
limits:
memory: 256Mi
cpu: 200m
requests:
memory: 128Mi
cpu: 100m
EOF
chmod 644 "$values_file"
log_success "Helm values configuration created"
}
# Install GitLab Runner with Helm
install_gitlab_runner() {
log_info "[7/7] Installing GitLab Runner with Helm..."
# Add GitLab Helm repository
helm repo add gitlab https://charts.gitlab.io
helm repo update
# Install GitLab Runner
if ! helm install gitlab-runner gitlab/gitlab-runner \
--namespace "$NAMESPACE" \
--values /tmp/gitlab-runner-values.yaml; then
log_error "Failed to install GitLab Runner"
exit 1
fi
log_success "GitLab Runner installed successfully"
}
# Verify installation
verify_installation() {
log_info "Verifying installation..."
# Wait for pods to be ready
local timeout=300
local elapsed=0
while [[ $elapsed -lt $timeout ]]; do
if kubectl get pods -n "$NAMESPACE" --no-headers | grep -q "Running"; then
log_success "GitLab Runner pods are running"
kubectl get pods -n "$NAMESPACE"
return 0
fi
sleep 10
elapsed=$((elapsed + 10))
log_info "Waiting for pods to start... ($elapsed/${timeout}s)"
done
log_warning "Timeout waiting for pods. Check status manually with:"
echo "kubectl get pods -n $NAMESPACE"
echo "kubectl logs -n $NAMESPACE -l app=gitlab-runner"
}
# Main function
main() {
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--gitlab-url)
GITLAB_URL="$2"
shift 2
;;
--runner-token)
RUNNER_TOKEN="$2"
shift 2
;;
-h|--help)
usage
;;
*)
log_error "Unknown option: $1"
usage
;;
esac
done
# Validate required arguments
if [[ -z "$GITLAB_URL" ]] || [[ -z "$RUNNER_TOKEN" ]]; then
log_error "Both --gitlab-url and --runner-token are required"
usage
fi
# Validate GitLab URL format
if [[ ! "$GITLAB_URL" =~ ^https?:// ]]; then
log_error "GitLab URL must start with http:// or https://"
exit 1
fi
log_info "Starting GitLab CI/CD with Kubernetes runners setup"
log_info "GitLab URL: $GITLAB_URL"
log_info "Namespace: $NAMESPACE"
check_privileges
detect_distro
install_prerequisites
install_kubectl
install_helm
create_rbac_config
apply_rbac
create_helm_values
install_gitlab_runner
verify_installation
# Cleanup temporary files
for file in "${CLEANUP_FILES[@]}"; do
[ -f "$file" ] && rm -f "$file"
done
log_success "GitLab Runner installation completed successfully!"
echo ""
echo "Next steps:"
echo "1. Verify runner registration in your GitLab project: Settings > CI/CD > Runners"
echo "2. Create a .gitlab-ci.yml file in your project to start using the runner"
echo "3. Monitor runner pods: kubectl get pods -n $NAMESPACE"
echo "4. Check runner logs: kubectl logs -n $NAMESPACE -l app=gitlab-runner"
}
main "$@"
Review the script before running. Execute with: bash install.sh