AI & Automation
PD Automation Runner: Installation
Deploy the PD Automation Runner into your infrastructure using Kubernetes YAML, Helm, Terraform, Docker Run, or Docker Compose.
This page covers how to deploy the PD Automation Runner into your infrastructure. For what the runner is, how it works, and how to register it and use it in a Workflow, see PD Automation Runner.
Before You Start
Register the runner in PagerDuty and have your three credential values ready — RUNNER_ID, RUNNER_SECRET, and RUNNER_PDTOKEN. In all commands on this page, replace <version> with the runner image version specified in your PagerDuty onboarding documentation.
Choose a Deployment Method
| Method | Runs As | Kubernetes Actions? |
|---|---|---|
| Kubernetes YAML | Pod inside your cluster | Yes (recommended) |
| Helm | Pod inside your cluster | Yes (chart not yet published) |
| Terraform | Pod inside your cluster | Yes |
| Docker Run | Standalone container | No |
| Docker Compose | Standalone container | No |
Kubernetes Actions Require In-Cluster Deployment
Kubernetes Workflow Actions authenticate to the Kubernetes API using the runner pod's service account token, so the runner must run as a pod inside the cluster it manages. Use Kubernetes YAML, Helm, or Terraform for Kubernetes actions.
Configure RBAC (Kubernetes)
The runner pod must have a Kubernetes service account with permission to perform the operations your Kubernetes Workflow Actions require. Using a service account (rather than a kubeconfig with personal credentials) is the correct pattern: tokens are automatically rotated, permissions are scoped and auditable, and no credential file needs to be managed.
The following ClusterRole grants the minimum permissions required for all six Kubernetes Workflow Actions. Apply it in place of any wildcard grant you may have used during initial setup. The Kubernetes YAML and Terraform methods in the following sections include these rules inline; this standalone copy is provided for reference or for applying RBAC separately.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pd-automation-runner
labels:
app.kubernetes.io/name: pd-automation-runner
app.kubernetes.io/part-of: pd-automation
rules:
# Core resources — list and describe support
- apiGroups: [""]
resources:
- configmaps
- namespaces
- nodes
- persistentvolumes
- persistentvolumeclaims
- secrets
- services
verbs: [get, list]
# Pods — all pod actions (list, describe, create, delete, logs, exec)
- apiGroups: [""]
resources: [pods]
verbs: [get, list, create, delete]
- apiGroups: [""]
resources: [pods/log]
verbs: [get]
- apiGroups: [""]
resources: [pods/exec]
verbs: [create]
# Apps resources
- apiGroups: [apps]
resources: [daemonsets, deployments, replicasets, statefulsets]
verbs: [get, list]
# Batch resources
- apiGroups: [batch]
resources: [cronjobs, jobs]
verbs: [get, list]
# Networking
- apiGroups: [networking.k8s.io]
resources: [ingresses]
verbs: [get, list]
# Policy — list-objects only (describe-object does not support PDBs)
- apiGroups: [policy]
resources: [poddisruptionbudgets]
verbs: [list]
# Storage
- apiGroups: [storage.k8s.io]
resources: [storageclasses]
verbs: [get, list]
# CRDs — list-objects only (describe-object does not support CRDs)
- apiGroups: [apiextensions.k8s.io]
resources: [customresourcedefinitions]
verbs: [list]
Secrets Access
The List Objects and Describe Object actions can target Kubernetes Secrets. The secrets: [get, list] entry gives the runner read access to all Secrets across the cluster when combined with a ClusterRoleBinding. If this is too broad for your security posture, restrict access to specific namespaces by using a RoleBinding (instead of a ClusterRoleBinding) in each namespace where the runner operates. A ClusterRoleBinding is only strictly required for cluster-scoped resources (Namespaces, Nodes, PersistentVolumes, Storage Classes, Custom Resource Definitions).
Option A: Kubernetes YAML (Recommended for Kubernetes Actions)
Deploy the runner using kubectl apply. This is the recommended method for Kubernetes Workflow Actions.
Step 1: Apply namespace and RBAC. Save the following as pd-runner-rbac.yaml and apply it:
apiVersion: v1
kind: Namespace
metadata:
name: pd-automation-runner
labels:
app.kubernetes.io/name: pd-automation-runner
app.kubernetes.io/part-of: pd-automation
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: pd-automation-runner
namespace: pd-automation-runner
labels:
app.kubernetes.io/name: pd-automation-runner
app.kubernetes.io/part-of: pd-automation
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: pd-automation-runner
labels:
app.kubernetes.io/name: pd-automation-runner
app.kubernetes.io/part-of: pd-automation
rules:
- apiGroups: [""]
resources:
- configmaps
- namespaces
- nodes
- persistentvolumes
- persistentvolumeclaims
- secrets
- services
verbs: [get, list]
- apiGroups: [""]
resources: [pods]
verbs: [get, list, create, delete]
- apiGroups: [""]
resources: [pods/log]
verbs: [get]
- apiGroups: [""]
resources: [pods/exec]
verbs: [create]
- apiGroups: [apps]
resources: [daemonsets, deployments, replicasets, statefulsets]
verbs: [get, list]
- apiGroups: [batch]
resources: [cronjobs, jobs]
verbs: [get, list]
- apiGroups: [networking.k8s.io]
resources: [ingresses]
verbs: [get, list]
- apiGroups: [policy]
resources: [poddisruptionbudgets]
verbs: [list]
- apiGroups: [storage.k8s.io]
resources: [storageclasses]
verbs: [get, list]
- apiGroups: [apiextensions.k8s.io]
resources: [customresourcedefinitions]
verbs: [list]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: pd-automation-runner
labels:
app.kubernetes.io/name: pd-automation-runner
app.kubernetes.io/part-of: pd-automation
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: pd-automation-runner
subjects:
- kind: ServiceAccount
name: pd-automation-runner
namespace: pd-automation-runner
kubectl apply -f pd-runner-rbac.yaml
Step 2: Create the credentials secret. Do not put credentials in a YAML file that could be committed to source control. Create the secret directly from your credential values:
kubectl create secret generic pd-automation-runner-credentials \
--namespace pd-automation-runner \
--from-literal=RUNNER_ID="<your-runner-id>" \
--from-literal=RUNNER_SECRET="<your-runner-secret>" \
--from-literal=RUNNER_PDTOKEN="<your-pd-api-token>"
Step 3: Deploy the runner. Save the following as pd-runner-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: pd-automation-runner
namespace: pd-automation-runner
labels:
app.kubernetes.io/name: pd-automation-runner
app.kubernetes.io/part-of: pd-automation
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: pd-automation-runner
template:
metadata:
labels:
app.kubernetes.io/name: pd-automation-runner
app.kubernetes.io/part-of: pd-automation
spec:
serviceAccountName: pd-automation-runner
automountServiceAccountToken: true
containers:
- name: runner
image: rundeckpro/runner:<version>
imagePullPolicy: IfNotPresent
env:
- name: RUNNER_ID
valueFrom:
secretKeyRef:
name: pd-automation-runner-credentials
key: RUNNER_ID
- name: RUNNER_SECRET
valueFrom:
secretKeyRef:
name: pd-automation-runner-credentials
key: RUNNER_SECRET
- name: RUNNER_PDTOKEN
valueFrom:
secretKeyRef:
name: pd-automation-runner-credentials
key: RUNNER_PDTOKEN
- name: RUNNER_CLOUD_URL
value: "https://api.pagerduty.com" # EU: https://api.eu.pagerduty.com
- name: RUNNER_LOG_OUTPUT
value: "console"
resources:
requests:
cpu: "100m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"
restartPolicy: Always
kubectl apply -f pd-runner-deployment.yaml
API Endpoint
RUNNER_CLOUD_URL defaults to the NA production endpoint (https://api.pagerduty.com). EU accounts should set it to https://api.eu.pagerduty.com. The value must match the region where the runner was registered — a runner registered in one region will not authenticate against another.
Step 4: Verify. Confirm the pod is running and follow its logs to confirm it connected to PagerDuty:
kubectl -n pd-automation-runner get pods
kubectl -n pd-automation-runner logs deploy/pd-automation-runner --follow
A successful connection produces log lines indicating the runner registered and is polling for jobs. Once connected, the runner appears as Healthy in Incident Workflows Automation Connectors Self-hosted Runners.
Updating. To update the runner image version, change the image: field in pd-runner-deployment.yaml and re-apply with kubectl apply -f pd-runner-deployment.yaml. Kubernetes performs a rolling update with zero downtime.
Removing. Delete the deployment, RBAC, secret, and namespace, then deregister the runner in Incident Workflows Automation Connectors Self-hosted Runners:
kubectl delete -f pd-runner-deployment.yaml
kubectl delete -f pd-runner-rbac.yaml
kubectl delete secret pd-automation-runner-credentials -n pd-automation-runner
kubectl delete namespace pd-automation-runner
Option B: Helm
Note
A PagerDuty-published Helm chart for the PD Automation Runner is not yet available. The commands in this section reflect the anticipated interface and will be updated when the chart is released. Use the Kubernetes YAML method in the meantime.
Add the repository:
helm repo add pagerduty https://charts.pagerduty.com
helm repo update
For production, create a credentials secret before installing so values are not stored in Helm release history:
kubectl create namespace pd-automation-runner
kubectl create secret generic pd-automation-runner-credentials \
--namespace pd-automation-runner \
--from-literal=RUNNER_ID="<your-runner-id>" \
--from-literal=RUNNER_SECRET="<your-runner-secret>" \
--from-literal=RUNNER_PDTOKEN="<your-pd-api-token>"
Then install referencing the existing secret (EU accounts set runner.cloudUrl to https://api.eu.pagerduty.com):
helm install pd-automation-runner pagerduty/pd-automation-runner \
--namespace pd-automation-runner \
--set runner.existingSecret="pd-automation-runner-credentials" \
--set runner.cloudUrl="https://api.pagerduty.com"
Verify the runner connected:
kubectl -n pd-automation-runner logs deploy/pd-automation-runner --follow
Key Chart Values
| Value | Description | Default |
|---|---|---|
runner.id | Runner ID from PagerDuty | — |
runner.secret | Runner secret from PagerDuty | — |
runner.pdToken | PagerDuty API token | — |
runner.cloudUrl | PagerDuty API endpoint. NA production default; EU customers use https://api.eu.pagerduty.com | https://api.pagerduty.com |
runner.existingSecret | Name of a pre-created credentials secret | — |
rbac.create | Create ServiceAccount, ClusterRole, ClusterRoleBinding | true |
rbac.clusterRole.rules | Override RBAC rules | Scoped to Kubernetes action permissions |
image.repository | Runner image repository | rundeckpro/runner |
image.tag | Runner image version | Chart default |
resources.requests.cpu | CPU request | 100m |
resources.requests.memory | Memory request | 256Mi |
resources.limits.cpu | CPU limit | 500m |
resources.limits.memory | Memory limit | 512Mi |
Option C: Terraform
Deploy the runner into a Kubernetes cluster using the Terraform Kubernetes provider. Create the credentials secret manually first — Terraform does not manage it, to avoid storing sensitive values in state:
kubectl create secret generic pd-automation-runner-credentials \
--namespace pd-automation-runner \
--from-literal=RUNNER_ID="<your-runner-id>" \
--from-literal=RUNNER_SECRET="<your-runner-secret>" \
--from-literal=RUNNER_PDTOKEN="<your-pd-api-token>"
EU Accounts
Add --from-literal=RUNNER_CLOUD_URL="https://api.eu.pagerduty.com" and reference it as an env var in the deployment, or set the runner_cloud_url variable in the following configuration.
Configure the provider in main.tf (the example uses a kubeconfig file; see the Terraform Kubernetes provider docs for EKS, GKE, and AKS authentication options):
terraform {
required_version = ">= 1.5"
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.30"
}
}
}
provider "kubernetes" {
config_path = "~/.kube/config"
config_context = "<your-kubectl-context>"
}
Declare variables in variables.tf:
variable "runner_image" {
description = "Runner Docker image to deploy"
type = string
default = "rundeckpro/runner:<version>"
}
variable "runner_cloud_url" {
description = "PagerDuty API endpoint the runner connects to"
type = string
default = "https://api.pagerduty.com"
}
Define the namespace, service account, RBAC, and deployment in runner.tf:
locals {
runner_name = "pd-automation-runner"
runner_namespace = "pd-automation-runner"
common_labels = {
"app.kubernetes.io/name" = "pd-automation-runner"
"app.kubernetes.io/part-of" = "pd-automation"
}
}
resource "kubernetes_namespace" "runner" {
metadata {
name = local.runner_namespace
labels = local.common_labels
}
}
resource "kubernetes_service_account" "runner" {
metadata {
name = local.runner_name
namespace = kubernetes_namespace.runner.metadata[0].name
labels = local.common_labels
}
automount_service_account_token = true
}
resource "kubernetes_cluster_role" "runner" {
metadata {
name = local.runner_name
labels = local.common_labels
}
rule {
api_groups = [""]
resources = ["configmaps", "namespaces", "nodes", "persistentvolumes", "persistentvolumeclaims", "secrets", "services"]
verbs = ["get", "list"]
}
rule {
api_groups = [""]
resources = ["pods"]
verbs = ["get", "list", "create", "delete"]
}
rule {
api_groups = [""]
resources = ["pods/log"]
verbs = ["get"]
}
rule {
api_groups = [""]
resources = ["pods/exec"]
verbs = ["create"]
}
rule {
api_groups = ["apps"]
resources = ["daemonsets", "deployments", "replicasets", "statefulsets"]
verbs = ["get", "list"]
}
rule {
api_groups = ["batch"]
resources = ["cronjobs", "jobs"]
verbs = ["get", "list"]
}
rule {
api_groups = ["networking.k8s.io"]
resources = ["ingresses"]
verbs = ["get", "list"]
}
rule {
api_groups = ["policy"]
resources = ["poddisruptionbudgets"]
verbs = ["list"]
}
rule {
api_groups = ["storage.k8s.io"]
resources = ["storageclasses"]
verbs = ["get", "list"]
}
rule {
api_groups = ["apiextensions.k8s.io"]
resources = ["customresourcedefinitions"]
verbs = ["list"]
}
}
resource "kubernetes_cluster_role_binding" "runner" {
metadata {
name = local.runner_name
labels = local.common_labels
}
role_ref {
api_group = "rbac.authorization.k8s.io"
kind = "ClusterRole"
name = kubernetes_cluster_role.runner.metadata[0].name
}
subject {
kind = "ServiceAccount"
name = kubernetes_service_account.runner.metadata[0].name
namespace = kubernetes_namespace.runner.metadata[0].name
}
}
resource "kubernetes_deployment" "runner" {
metadata {
name = local.runner_name
namespace = kubernetes_namespace.runner.metadata[0].name
labels = local.common_labels
}
spec {
replicas = 1
selector {
match_labels = {
"app.kubernetes.io/name" = local.runner_name
}
}
template {
metadata {
labels = local.common_labels
}
spec {
service_account_name = kubernetes_service_account.runner.metadata[0].name
automount_service_account_token = true
container {
name = "runner"
image = var.runner_image
image_pull_policy = "IfNotPresent"
env {
name = "RUNNER_ID"
value_from {
secret_key_ref {
name = "pd-automation-runner-credentials"
key = "RUNNER_ID"
}
}
}
env {
name = "RUNNER_SECRET"
value_from {
secret_key_ref {
name = "pd-automation-runner-credentials"
key = "RUNNER_SECRET"
}
}
}
env {
name = "RUNNER_PDTOKEN"
value_from {
secret_key_ref {
name = "pd-automation-runner-credentials"
key = "RUNNER_PDTOKEN"
}
}
}
env {
name = "RUNNER_CLOUD_URL"
value = var.runner_cloud_url
}
env {
name = "RUNNER_LOG_OUTPUT"
value = "console"
}
resources {
requests = {
cpu = "100m"
memory = "256Mi"
}
limits = {
cpu = "500m"
memory = "512Mi"
}
}
}
restart_policy = "Always"
}
}
}
depends_on = [kubernetes_cluster_role_binding.runner]
}
Apply and verify:
terraform init
terraform plan
terraform apply
kubectl -n pd-automation-runner logs deploy/pd-automation-runner --follow
Option D: Docker Run
Kubernetes Actions Are Not Supported with Docker Deployment
Kubernetes Workflow Actions require the runner to run as a pod inside the target cluster. Use the Kubernetes YAML method for Kubernetes actions.
docker run -d \
--name pd-automation-runner \
--restart unless-stopped \
-e RUNNER_ID="<your-runner-id>" \
-e RUNNER_SECRET="<your-runner-secret>" \
-e RUNNER_PDTOKEN="<your-pd-api-token>" \
-e RUNNER_CLOUD_URL="https://api.pagerduty.com" \
-e RUNNER_LOG_OUTPUT="console" \
rundeckpro/runner:<version>
EU Accounts
Set RUNNER_CLOUD_URL to https://api.eu.pagerduty.com.
Verify the runner connected:
docker logs -f pd-automation-runner
Option E: Docker Compose
Kubernetes Actions Are Not Supported with Docker Deployment
Use the Kubernetes YAML method for Kubernetes actions.
Create a .env file with your credentials (do not commit it to source control):
RUNNER_ID=<your-runner-id>
RUNNER_SECRET=<your-runner-secret>
RUNNER_PDTOKEN=<your-pd-api-token>
Create docker-compose.yml (EU accounts set RUNNER_CLOUD_URL to https://api.eu.pagerduty.com):
services:
pd-automation-runner:
image: rundeckpro/runner:<version>
container_name: pd-automation-runner
restart: unless-stopped
environment:
RUNNER_ID: "${RUNNER_ID}"
RUNNER_SECRET: "${RUNNER_SECRET}"
RUNNER_PDTOKEN: "${RUNNER_PDTOKEN}"
RUNNER_CLOUD_URL: "https://api.pagerduty.com"
RUNNER_LOG_OUTPUT: "console"
Start the runner and verify it connected:
docker compose up -d
docker compose logs -f pd-automation-runner
In all cases, the runner appears as Healthy in Incident Workflows Automation Connectors Self-hosted Runners once connected.
Troubleshooting
Runner Shows Unhealthy in PagerDuty
The runner appears in Incident Workflows Automation Connectors Self-hosted Runners but its status is Unhealthy, or it does not connect at all. Check the runner logs first:
kubectl -n pd-automation-runner logs deploy/pd-automation-runner --follow
| Symptom in Logs | Cause | Fix |
|---|---|---|
401 Unauthorized or authentication failed | Invalid RUNNER_ID, RUNNER_SECRET, or RUNNER_PDTOKEN | Verify credentials match what PagerDuty generated at registration. Re-create the secret if needed. |
connection refused or unable to connect | Network egress blocked | The runner requires outbound HTTPS (port 443) to the PagerDuty API endpoint configured in RUNNER_CLOUD_URL. Check firewall and proxy rules. |
Pod is in CrashLoopBackOff | Container exiting on startup | Run kubectl -n pd-automation-runner describe pod <pod-name> for the exit reason. Usually a missing or malformed secret. |
| No log output at all | Pod not starting | Run kubectl -n pd-automation-runner get pods to check pod status, then describe for events. |
Action Fails with "Runner Offline" or Times Out
A Workflow action returns Failed with a message indicating the runner did not respond.
- Confirm the runner pod is running:
kubectl -n pd-automation-runner get pods - Confirm the runner shows Healthy in Incident Workflows Automation Connectors Self-hosted Runners.
- Confirm the Connection Input in the Workflow action references the correct runner.
- Check runner logs for a received job — a healthy runner logs each job it receives.
Action Fails with a Permissions Error
The action executes but returns an error like forbidden: User "system:serviceaccount:pd-automation-runner:pd-automation-runner" cannot list resource "pods". The runner's service account does not have the required RBAC permissions.
- Verify the ClusterRole and ClusterRoleBinding exist:
kubectl get clusterrole pd-automation-runner
kubectl get clusterrolebinding pd-automation-runner
- Check the ClusterRole rules match the scoped manifest in the Configure RBAC section. If you started with a wildcard grant and removed it, ensure the replacement rules cover the resource type the action is trying to access.
- If you switched from a ClusterRoleBinding to per-namespace RoleBindings, verify a RoleBinding exists in the namespace the action is targeting:
kubectl get rolebinding -n <target-namespace> | grep pd-automation-runner
- Apply the corrected RBAC and restart the runner:
kubectl apply -f pd-runner-rbac.yaml
kubectl -n pd-automation-runner rollout restart deploy/pd-automation-runner
Action Fails with "Unknown Object Type" or Similar Plugin Error
The runner received the job but the Kubernetes plugin could not process it.
- Check runner logs for the specific error message around the time of the action invocation.
- Verify the Object Type input matches a supported type for that action (refer to the input tables in Kubernetes Workflow Actions).
- Verify the Namespace input is correct for namespace-scoped resources.
Credentials Secret Is Missing or Malformed
Symptom: the pod goes into CrashLoopBackOff immediately and logs show a missing environment variable. Verify the secret exists and contains exactly these keys — RUNNER_ID, RUNNER_SECRET, RUNNER_PDTOKEN:
kubectl get secret pd-automation-runner-credentials -n pd-automation-runner
kubectl describe secret pd-automation-runner-credentials -n pd-automation-runner
To re-create the secret:
kubectl delete secret pd-automation-runner-credentials -n pd-automation-runner
kubectl create secret generic pd-automation-runner-credentials \
--namespace pd-automation-runner \
--from-literal=RUNNER_ID="<your-runner-id>" \
--from-literal=RUNNER_SECRET="<your-runner-secret>" \
--from-literal=RUNNER_PDTOKEN="<your-pd-api-token>"
kubectl -n pd-automation-runner rollout restart deploy/pd-automation-runner
Checking Runner Health in PagerDuty
Navigate to Incident Workflows Automation Connectors Self-hosted Runners and select the runner by name. The detail page shows its Status (Healthy / Unhealthy), Last seen timestamp (most recent heartbeat), and Active jobs currently being processed. If the runner is Healthy but actions are still failing, the issue is likely in the action's configuration (wrong connection input, wrong namespace, or insufficient RBAC) rather than the runner itself.