|docs
Start Trial

AI & Automation

PD Automation Runner: Installation

Deploy the PD Automation Runner into your infrastructure using Kubernetes YAML, Helm, Terraform, Docker Run, or Docker Compose.

View as Markdown

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

MethodRuns AsKubernetes Actions?
Kubernetes YAMLPod inside your clusterYes (recommended)
HelmPod inside your clusterYes (chart not yet published)
TerraformPod inside your clusterYes
Docker RunStandalone containerNo
Docker ComposeStandalone containerNo

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).

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

ValueDescriptionDefault
runner.idRunner ID from PagerDuty
runner.secretRunner secret from PagerDuty
runner.pdTokenPagerDuty API token
runner.cloudUrlPagerDuty API endpoint. NA production default; EU customers use https://api.eu.pagerduty.comhttps://api.pagerduty.com
runner.existingSecretName of a pre-created credentials secret
rbac.createCreate ServiceAccount, ClusterRole, ClusterRoleBindingtrue
rbac.clusterRole.rulesOverride RBAC rulesScoped to Kubernetes action permissions
image.repositoryRunner image repositoryrundeckpro/runner
image.tagRunner image versionChart default
resources.requests.cpuCPU request100m
resources.requests.memoryMemory request256Mi
resources.limits.cpuCPU limit500m
resources.limits.memoryMemory limit512Mi

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 LogsCauseFix
401 Unauthorized or authentication failedInvalid RUNNER_ID, RUNNER_SECRET, or RUNNER_PDTOKENVerify credentials match what PagerDuty generated at registration. Re-create the secret if needed.
connection refused or unable to connectNetwork egress blockedThe runner requires outbound HTTPS (port 443) to the PagerDuty API endpoint configured in RUNNER_CLOUD_URL. Check firewall and proxy rules.
Pod is in CrashLoopBackOffContainer exiting on startupRun kubectl -n pd-automation-runner describe pod <pod-name> for the exit reason. Usually a missing or malformed secret.
No log output at allPod not startingRun 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.

  1. Confirm the runner pod is running: kubectl -n pd-automation-runner get pods
  2. Confirm the runner shows Healthy in Incident Workflows Automation Connectors Self-hosted Runners.
  3. Confirm the Connection Input in the Workflow action references the correct runner.
  4. 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.

  1. Verify the ClusterRole and ClusterRoleBinding exist:
kubectl get clusterrole pd-automation-runner
kubectl get clusterrolebinding pd-automation-runner
  1. 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.
  2. 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
  1. 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.

  1. Check runner logs for the specific error message around the time of the action invocation.
  2. Verify the Object Type input matches a supported type for that action (refer to the input tables in Kubernetes Workflow Actions).
  3. 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.

Learn More