kubernetes-agent

The kubernetes-agent runner type lets you execute runners on Kubernetes clusters which do not have a publicly accessible API server.

The kubernetes-agent is a runner agent which runs in your cluster and establishes a secure connection to the Platform Orchestrator. Once connected, the kubernetes-agent runner polls the Orchestrator for events and triggers Kubernetes Jobs in the same cluster where it’s running in response to them.

Follow the steps below to configure a runner of type kubernetes-agent.

Example configuration


resource.tf (view on GitHub ) :

resource "platform-orchestrator_kubernetes_agent_runner" "my_runner" {
  id          = "my-runner"
  description = "runner for all the envs"
  runner_configuration = {
    key = <<EOT
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAc5dgCx4ano39JT0XgTsHnts3jej+5xl7ZAwSIrKpef0=
-----END PUBLIC KEY-----
EOT
    job = {
      namespace       = "default"
      service_account = "humanitec-runner"
      pod_template = jsonencode({
        metadata = {
          labels = {
            "app.kubernetes.io/name" = "humanitec-runner"
          }
        }
      })
    }
  }
  state_storage_configuration = {
    type = "kubernetes"
    kubernetes_configuration = {
      namespace = "humanitec"
    }
  }
}

hctl create runner my-runner [email protected]

where runner-config.yaml is:

runner_configuration:
  type: kubernetes-agent
  key: |
    -----BEGIN PUBLIC KEY-----
    MCowBQYDK2VwAyEAc5dgCx4ano39JT0XgTsHnts3jej+5xl7ZAwSIrKpef0=
    -----END PUBLIC KEY-----
  job:
    namespace: default
    service_account: humanitec-runner
    pod_template:
      metadata:
        labels:
          app.kubernetes.io/name: humanitec-runner
state_storage_configuration:
  type: kubernetes
  namespace: humanitec
  ...

See all configuration options further down.

Configuration options

The following configuration shows all available options for the kubernetes-agent runner type:

Refer to the resource schema in the Terraform  or OpenTofu  provider documentation.

Extended example

resource.tf (view on GitHub ) :

resource "platform-orchestrator_kubernetes_agent_runner" "my_runner" {
  id          = "my-runner"
  description = "runner for all the envs"
  runner_configuration = {
    key = <<EOT
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAc5dgCx4ano39JT0XgTsHnts3jej+5xl7ZAwSIrKpef0=
-----END PUBLIC KEY-----
EOT
    job = {
      namespace       = "default"
      service_account = "humanitec-runner"
      pod_template = jsonencode({
        metadata = {
          labels = {
            "app.kubernetes.io/name" = "humanitec-runner"
          }
        }
      })
    }
  }
  state_storage_configuration = {
    type = "kubernetes"
    kubernetes_configuration = {
      namespace = "humanitec"
    }
  }
}

runner_configuration:
  # Runner type
  type: kubernetes-agent

  # Kubernetes job configuration
  job:
    # Public key used to verify the runner agent's identity
    key: |
      -----BEGIN PUBLIC KEY-----
      ...
      -----END PUBLIC KEY-----

    # Namespace where runner jobs run
    namespace: humanitec-runner

    # Service account to use for the runner jobs
    service_account: humanitec-runner

    # (optional) Pod template for customizing runner job pods
    pod_template:
      # Add custom pod specifications here
      # See Kubernetes PodSpec documentation for available options
      ...

# State storage configuration
state_storage_configuration:
  # Add state storeage configuration here
  # See State storage types documentation for available options
  ...

Connectivity

The Kubernetes node(s) hosting the runner require network access to the Orchestrator API.

For the SaaS version of the Orchestrator, refer to the public endpoint IPs to configure network access.

For a self-hosted Orchestrator, refer to your own networking layout.

See connectivity for more details.

Setup guide

The steps below serve as a general guide to configure the runner using either Terraform/OpenTofu (“TF”) code or the CLI. Adjust the steps as needed based on your environment and requirements.

The setup guide provides specific instructions for different cluster types:

Perform the steps in the section that corresponds to your Kubernetes cluster type.

Self-hosting requirements

You need to make the required runner components available in your own infrastructure when self-hosting the Orchestrator. This includes:

  • An internal Git service to host a repository for the TF runner module
  • An internal OCI artefact registry to host the runner Helm chart and runner container image

Instructions how to obtain and import each component are shown below. Review and execute these before proceeding to the actual Kubernetes setup.

Orchestrator API endpoint

Obtain the Orchestrator API endpoint out of your self-hosting setup of the Orchestrator. The setup instructions for each cluster type further down on this page will ask you to provide it at the appropriate place.

TF module

Make the public kubernetes-agent-orchestrator-runner TF module  repository available in a private Git service within your infrastructure.

# Clone the repository (latest)
git clone https://github.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner.git

# Or use a ref to clone a particular release
# git clone --branch vX.Y.Z --single-branch https://github.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner.git

# Use git mechanisms at your discretion to create a new repo and push the contents
# ...

If feasible, you may also fork the repo to your system.

If your private Git service requires authentication, you need to configure the runner to provide credentials. Follow the instructions for private module sources.

Helm chart

Make the public humanitec-kubernetes-agent-runner Helm chart available in an OCI registry within your infrastructure.

# Pull the Helm chart
helm pull oci://ghcr.io/humanitec/charts/humanitec-kubernetes-agent-runner
# Or pull a particular chart version
# helm pull oci://ghcr.io/humanitec/charts/humanitec-kubernetes-agent-runner --version X.Y.Z

# Push the Helm chart to your internal registry
# Adjust your registry domain and charts path according to your setup
helm push humanitec-kubernetes-agent-runner-X.Y.Z.tgz oci://myregistry.example.com/charts

The chart must be named humanitec-kubernetes-agent-runner in your registry.

If your registry requires authentication, refer to the helm push command  for details on pushing the chart using credentials, or execute a helm registry login command .

Also, If your registry requires authentication, the client executing the TF apply command for setting up the runner will have to provide credentials to Helm for pulling the chart. The setup instructions for each cluster type below include the details.

Runner image

Make the public humanitec-runner container image  available in an OCI registry within your infrastructure.

# Pull the desired version
docker image pull ghcr.io/humanitec/humanitec-runner:vX.Y.Z

# Tag the image so that it points to your internal registry
# Adjust your registry domain and image path according to your setup
docker image tag ghcr.io/humanitec/humanitec-runner:vX.Y.Z myregistry.example.com/humanitec/humanitec-runner:vX.Y.Z

# Push the image to your internal registry
# Adjust your registry domain and image path according to your setup
docker image push myregistry.example.com/humanitec/humanitec-runner:vX.Y.Z

If your registry requires authentication, refer to the docker login  command.

You will also need to configure the runner to use your image source. Refer to runner image for instructions.

Generic Kubernetes setup

This section describes the installation of the kubernetes-agent runner on a cluster type other than the specific ones covered on this page.

To have the kubernetes-agent runner in your Kubernetes cluster, you need:

  1. Declare the required providers:
terraform {
  required_providers {
    # Providers needed to install the kubernetes-agent runner Helm chart and register it with the Orchestrator
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2"
    }
    platform-orchestrator = {
      source  = "humanitec/platform-orchestrator"
      version = "~> 2"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "~> 3"
    }
    local = {
      source  = "hashicorp/local"
      version = ">= 2.0"
    }
  }
}
  1. Set values according to your setup. Change the default values at your discretion:
locals {
  kubeconfig_context = "<context-in-your-kubeconfig>"
  humanitec_org      = "<my-org>"
  runner_id          = "kubernetes-agent-${local.kubeconfig_context}" # Adjust the ID at your discretion

  # The Kubernetes namespace where the kubernetes-agent runner should run
  cluster_runner_namespace = "humanitec-kubernetes-agent-runner-ns"

  # The name of the Kubernetes service account to be assumed by the kubernetes-agent runner
  cluster_runner_serviceaccount = "humanitec-kubernetes-agent-runner"

  # The Kubernetes namespace where the deployment jobs should run
  cluster_runner_job_namespace = "humanitec-kubernetes-agent-runner-job-ns"

  # The name of the Kubernetes service account to be assumed by the deployment jobs created by the kubernetes-agent runner
  cluster_runner_job_serviceaccount = "humanitec-kubernetes-agent-runner-job"
}

Use TF variables of your module instead of placing the raw values in the locals at your discretion.

In a self-hosting setup only, add and adjust these additional values. Remove those that do not apply:

locals {
  ###
  # Parameters for a self-hosting setup
  ###
  #
  # Your internal OCI registry for the runner Helm chart
  kubernetes_agent_runner_chart_repository = "oci://myregistry.example.com/charts"
  #
  # Runner Helm chart version (optional)
  kubernetes_agent_runner_chart_version = "X.Y.Z"
  #
  # Your internal OCI registry for the runner image
  kubernetes_agent_runner_image_repository = "myregistry.example.com/humanitec/humanitec-runner"
  #
  # Runner image tag (optional)
  kubernetes_agent_runner_image_tag = "vX.Y.Z"
  #
  # Your internal Orchestrator API endpoint
  orchestrator_api_url = "https://my-orchestrator-api.example.com"
}
  1. Configure providers:
# Platform Orchestrator provider. Authentication taken from local "hctl" CLI
provider "platform-orchestrator" {
  org_id = local.humanitec_org
}

# Kubernetes provider. Authentication taken from local kubeconfig context
# Check the provider documentation for alternative ways of authentication
provider "kubernetes" {
  config_path    = "~/.kube/config" # Adjust if needed or remove if default works
  config_context = local.kubeconfig_context
}

# Helm provider. Kubernetes authentication taken from local kubeconfig context
# Check the provider documentation for alternative ways of authentication
provider "helm" {
  kubernetes = {
    config_path    = "~/.kube/config" # Adjust if needed or remove if default works
    config_context =  local.kubeconfig_context
  }
}

If you are self-hosting the Helm chart in your own registry and that registry requires authentication, you need to configure credentials for Helm. There are several options.

variable "helm_repository_username" {
  type = string
}
variable "helm_repository_password" {
  type      = string
  sensitive = true
}
provider "helm" {
  # ...
  registries = [{
    url      = local.kubernetes_agent_runner_chart_repository
    username = var.helm_repository_username
    password = var.helm_repository_password
  }]
}

Pass the variable values using standard TF mechanisms, e.g. set the TF_VAR_helm_repository_username and TF_VAR_helm_repository_password environment variables on the client executing the TF apply.

  • Option 2: Use the helm CLI to login to the registry on the client executing the TF apply:
export HELM_REGISTRY_PASSWORD=<your-password>
echo $HELM_REGISTRY_PASSWORD | helm registry login myregistry.example.com -u admin --password-stdin

See the documentation on the helm registry login command  for supported ways to log in.

  1. Set values
export HUMANITEC_ORG=<my-org>
export KUBERNETES_AGENT_RUNNER_CHART_REPOSITORY="oci://ghcr.io/humanitec/charts"

# The Kubernetes namespace where the kubernetes-agent runner should run
export CLUSTER_RUNNER_NAMESPACE="humanitec-kubernetes-agent-runner-ns"

# The name of the Kubernetes service account to be assumed by the kubernetes-agent runner
export CLUSTER_RUNNER_SERVICEACCOUNT="humanitec-kubernetes-agent-runner"

# The Kubernetes namespace where the deployment jobs should run
export CLUSTER_RUNNER_JOB_NAMESPACE="humanitec-kubernetes-agent-runner-job-ns"

# The name of the Kubernetes service account to be assumed by the deployment jobs created by the kubernetes-agent runner
export CLUSTER_RUNNER_JOB_SERVICEACCOUNT="humanitec-kubernetes-agent-runner-job"

In a self-hosting setup only, add and adjust these additional values. Remove those that do not apply:

###
# Parameters for a self-hosting setup
###
# Override the Helm chart registry with your internal OCI registry
export KUBERNETES_AGENT_RUNNER_CHART_REPOSITORY="oci://myregistry.example.com/charts"
# Runner Helm chart version (optional)
export KUBERNETES_AGENT_RUNNER_CHART_VERSION="X.Y.Z"
# Your internal OCI registry for the runner image
export KUBERNETES_AGENT_RUNNER_IMAGE_REPOSITORY="myregistry.example.com/humanitec/humanitec-runner"
# Runner image tag (optional)
export KUBERNETES_AGENT_RUNNER_IMAGE_TAG="vX.Y.Z"
# Your internal Orchestrator API endpoint
export ORCHESTRATOR_API_URL="https://my-orchestrator-api.example.com"

A common part for both TF and CLI setups is the need for a private/public key pair for the runner to authenticate against the Orchestrator:

openssl genpkey -algorithm ed25519 -out runner_private_key.pem
openssl pkey -in runner_private_key.pem -pubout -out runner_public_key.pem

  1. Define the runner resource using the public Humanitec module and the key pair to install the runner into the cluster and register it with the Orchestrator:
module "kubernetes_agent_runner" {
  source = "github.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner"
  # To pin a specific module release, use this notation:
  # source = "github.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner?ref=vX.Y.Z"

  humanitec_org_id             = local.humanitec_org
  runner_id                    = local.runner_id
  private_key_path             = "./runner_private_key.pem"
  public_key_path              = "./runner_public_key.pem"
  k8s_namespace                = local.cluster_runner_namespace
  k8s_service_account_name     = local.cluster_runner_serviceaccount
  k8s_job_namespace            = local.cluster_runner_job_namespace
  k8s_job_service_account_name = local.cluster_runner_job_serviceaccount
}

# Provide the runner ID as an output
output "runner_id" {
  value = module.kubernetes_agent_runner.runner_id
}

Visit the module documentation  to see further configuration options and available releases.

In a self-hosting setup only, apply these additional changes to the module. Remove those that do not apply:

module "kubernetes_agent_runner" {
  # Use an internally hosted registry for the TF module. Append "?ref=vX.Y.Z" to pin a specific release
  source = "mygitservice.example.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner"
  #
  # Existing properties ...
  #
  ###
  # Parameters for a self-hosting setup
  # Remove those that do not apply
  ###
  # Use an internally hosted Helm chart registry
  kubernetes_agent_runner_chart_repository = local.kubernetes_agent_runner_chart_repository
  kubernetes_agent_runner_chart_version    = local.kubernetes_agent_runner_chart_version
  #
  # Use the API endpoint of a self-hosted Orchestrator
  extra_env_vars = [
    {
      name  = "REMOTE_URL"
      value = local.orchestrator_api_url
    }
  ]
  #
  # Use an internally hosted image registry
  kubernetes_agent_runner_image_repository = local.kubernetes_agent_runner_image_repository
  kubernetes_agent_runner_image_tag        = local.kubernetes_agent_runner_image_tag
  pod_template = jsonencode({
    metadata = {
      labels = {
        "app.kubernetes.io/name"    = "humanitec-runner"
        "app.kubernetes.io/version" = local.kubernetes_agent_runner_image_tag
      }
    }
    spec = {
      containers = [
        {
          name            = "main" # Use this exact value
          image           = "${local.kubernetes_agent_runner_image_repository}:${local.kubernetes_agent_runner_image_tag}"
          imagePullPolicy = "IfNotPresent"
        }
      ]
      # If your registry requires authentication, configure and reference an image pull secret
      # Otherwise remove the imagePullSecrets property
      imagePullSecrets = [
        {
          name = "regcred"
        }
      ]
    }
  })
}
  1. Assign any permissions the runner will need to provision resources:

See assign runner permissions for guidance.

  1. Verify the kubectl context is set to your target cluster:
kubectl config current-context
  1. Initialize and apply the TF configuration:
Terraform
terraform init
terraform apply
OpenTofu
tofu init
tofu apply

  1. Verify the kubectl context is set to your target cluster:
kubectl config current-context
  1. Set values:
export KUBECONFIG_CONTEXT=$(kubectl config current-context)
export RUNNER_ID=kubernetes-agent-${KUBECONFIG_CONTEXT}  # Adjust the ID at your discretion
  1. Create the namespace for the runner and a secret holding the runner private key:
kubectl create namespace ${CLUSTER_RUNNER_NAMESPACE}

kubectl create secret generic humanitec-kubernetes-agent-runner \
  -n ${CLUSTER_RUNNER_NAMESPACE} \
  --from-literal=private_key="$(cat runner_private_key.pem)"
  1. Create the namespace for the runner jobs:
kubectl create namespace ${CLUSTER_RUNNER_JOB_NAMESPACE}
  1. Register the kubernetes-agent runner with the Orchestrator:
hctl create runner ${RUNNER_ID} \
  --set=runner_configuration="$(jq -nc --arg key "$(cat runner_public_key.pem)" '{"type": "kubernetes-agent","key":$key,"job":{"namespace":"'${CLUSTER_RUNNER_JOB_NAMESPACE}'","service_account":"'${CLUSTER_RUNNER_JOB_SERVICEACCOUNT}'"}}')" \
  --set=state_storage_configuration='{"type":"kubernetes","namespace":"'${CLUSTER_RUNNER_JOB_NAMESPACE}'"}'
  1. Install the runner Helm chart onto the cluster, providing required values:
helm install humanitec-kubernetes-agent-runner \
  ${KUBERNETES_AGENT_RUNNER_CHART_REPOSITORY}/humanitec-kubernetes-agent-runner \
  -n ${CLUSTER_RUNNER_NAMESPACE} \
  --set humanitec.orgId=${HUMANITEC_ORG} \
  --set humanitec.runnerId=${RUNNER_ID} \
  --set humanitec.existingSecret=humanitec-kubernetes-agent-runner \
  --set namespaceOverride=${CLUSTER_RUNNER_NAMESPACE} \
  --set serviceAccount.name=${CLUSTER_RUNNER_SERVICEACCOUNT} \
  --set jobsRbac.namespace=${CLUSTER_RUNNER_JOB_NAMESPACE} \
  --set jobsRbac.serviceAccountName=${CLUSTER_RUNNER_JOB_SERVICEACCOUNT}

In a self-hosting setup only, append these settings to the helm install command. Remove those that do not apply:

  --version ${KUBERNETES_AGENT_RUNNER_CHART_VERSION} \
  --set image.repository=${KUBERNETES_AGENT_RUNNER_IMAGE_REPOSITORY} \
  --set image.tag=${KUBERNETES_AGENT_RUNNER_IMAGE_TAG} \
  --set-json humanitec.extraEnvVars='[{"name":"REMOTE_URL","value":"'${ORCHESTRATOR_API_URL}'"}]' \
  # ... use all other values as shown above
  1. Assign any permissions the runner will need to provision resources:

See assign runner permissions for guidance.

Your kubernetes-agent runner is now ready to be used. Continue by defining runner rules to assign the runner to environments and execute deployments.

EKS-specific setup

To have the kubernetes-agent runner in your EKS cluster, you need:

  • An existing AWS EKS cluster 
  • The aws CLI  installed and authenticated with a principal having permission to manage IAM roles and policies in the AWS account of the EKS cluster
  • The hctl CLI installed and authenticated against your Orchestrator organization
  • kubectl  installed locally and the current context set to the target cluster
  • Depending on your choice of tooling:

The setup utilizes IAM roles for service accounts (IRSA)  for the EKS cluster to provide credentials to the runner for TF execution.

  1. Declare the required providers:
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6"
    }
    # Providers needed to install the kubernetes-agent runner Helm chart and register it with the Orchestrator
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2"
    }
    platform-orchestrator = {
      source  = "humanitec/platform-orchestrator"
      version = "~> 2"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "~> 3"
    }
    local = {
      source  = "hashicorp/local"
      version = ">= 2.0"
    }
  }
}
  1. Set values according to your setup. Change the default values at your discretion:
locals {
  aws_account_id = "<my-aws-account-id>"
  aws_region     = "<my-aws-region>"
  cluster_name   = "<my-eks-cluster-name>"
  humanitec_org  = "<my-org>"
  runner_id      = "kubernetes-agent-${local.cluster_name}" # Adjust the ID at your discretion

  # The Kubernetes namespace where the kubernetes-agent runner should run
  cluster_runner_namespace = "humanitec-kubernetes-agent-runner-ns"

  # The name of the Kubernetes service account to be assumed by the kubernetes-agent runner
  cluster_runner_serviceaccount = "humanitec-kubernetes-agent-runner"

  # The Kubernetes namespace where the deployment jobs should run
  cluster_runner_job_namespace = "humanitec-kubernetes-agent-runner-job-ns"

  # The name of the Kubernetes service account to be assumed by the deployment jobs created by the kubernetes-agent runner
  cluster_runner_job_serviceaccount = "humanitec-kubernetes-agent-runner-job"
}

Use TF variables of your module instead of placing the raw values in the locals at your discretion.

In a self-hosting setup only, add and adjust these additional values. Remove those that do not apply:

locals {
  ###
  # Parameters for a self-hosting setup
  ###
  #
  # Your internal OCI registry for the runner Helm chart
  kubernetes_agent_runner_chart_repository = "oci://myregistry.example.com/charts"
  #
  # Runner Helm chart version (optional)
  kubernetes_agent_runner_chart_version = "X.Y.Z"
  #
  # Your internal OCI registry for the runner image
  kubernetes_agent_runner_image_repository = "myregistry.example.com/humanitec/humanitec-runner"
  #
  # Runner image tag (optional)
  kubernetes_agent_runner_image_tag = "vX.Y.Z"
  #
  # Your internal Orchestrator API endpoint
  orchestrator_api_url = "https://my-orchestrator-api.example.com"
}
  1. Create a data source from the existing cluster and extract required cluster properties:
# Data source for the existing EKS cluster
data "aws_eks_cluster" "cluster" {
  name = local.cluster_name
}

# Extract cluster properties
locals {
  oidc_provider_url      = data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer
  oidc_provider          = trimprefix(data.aws_eks_cluster.cluster.identity[0].oidc[0].issuer, "https://")
  cluster_endpoint       = data.aws_eks_cluster.cluster.endpoint
  cluster_ca_certificate = data.aws_eks_cluster.cluster.certificate_authority[0].data
}
  1. Configure providers:
# AWS provider. Authentication taken from local "aws" CLI
provider "aws" {
  region = local.aws_region
}

# Platform Orchestrator provider. Authentication taken from local "hctl" CLI
provider "platform-orchestrator" {
  org_id = local.humanitec_org
}

# Kubernetes provider. Authentication taken from local "aws" CLI
provider "kubernetes" {
  host                   = local.cluster_endpoint
  cluster_ca_certificate = base64decode(local.cluster_ca_certificate)
  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    args        = ["eks", "get-token", "--output", "json", "--cluster-name", local.cluster_name, "--region", local.aws_region]
    command     = "aws"
  }
}

# Helm provider. Kubernetes authentication taken from local "aws" CLI
provider "helm" {
  kubernetes = {
    host                   = local.cluster_endpoint
    cluster_ca_certificate = base64decode(local.cluster_ca_certificate)
    exec = {
      api_version = "client.authentication.k8s.io/v1beta1"
      args        = ["eks", "get-token", "--output", "json", "--cluster-name", local.cluster_name, "--region", local.aws_region]
      command     = "aws"
    }
  }
}

If you are self-hosting the Helm chart in your own registry and that registry requires authentication, you need to configure credentials for Helm. There are several options.

variable "helm_repository_username" {
  type = string
}
variable "helm_repository_password" {
  type      = string
  sensitive = true
}
provider "helm" {
  # ...
  registries = [{
    url      = local.kubernetes_agent_runner_chart_repository
    username = var.helm_repository_username
    password = var.helm_repository_password
  }]
}

Pass the variable values using standard TF mechanisms, e.g. set the TF_VAR_helm_repository_username and TF_VAR_helm_repository_password environment variables on the client executing the TF apply.

  • Option 2: Use the helm CLI to login to the registry on the client executing the TF apply:
export HELM_REGISTRY_PASSWORD=<your-password>
echo $HELM_REGISTRY_PASSWORD | helm registry login myregistry.example.com -u admin --password-stdin

See the documentation on the helm registry login command  for supported ways to log in.

  1. Create an IAM role for IRSA that allows the Kubernetes service account used by the runner jobs to assume it:
data "aws_iam_policy_document" "assume_role" {
  version = "2012-10-17"
  statement {
    effect = "Allow"
    principals {
      type        = "Federated"
      identifiers = ["arn:aws:iam::${local.aws_account_id}:oidc-provider/${local.oidc_provider}"]
    }
    actions = [
      "sts:AssumeRoleWithWebIdentity"
    ]
    condition {
      test     = "StringEquals"
      variable = "${local.oidc_provider}:aud"
      values = [
        "sts.amazonaws.com"
      ]
    }
    condition {
      test     = "StringEquals"
      variable = "${local.oidc_provider}:sub"
      values = [
        "system:serviceaccount:${local.cluster_runner_job_namespace}:${local.cluster_runner_job_serviceaccount}"
      ]
    }
  }
}

# This role will be assumed by the runner jobs
resource "aws_iam_role" "agent_runner_irsa_role" {
  name               = "humanitec-kubernetes-agent-runner-${local.cluster_name}"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
  1. Create and associate an IAM OIDC provider for your cluster:

Skip this step if you already have an IAM OIDC provider associated to your EKS cluster. To determine whether you do, refer to the AWS documentation .

resource "aws_iam_openid_connect_provider" "agent_runner" {
  url   = local.oidc_provider_url
  client_id_list = [
    "sts.amazonaws.com"
  ]
}

  1. Set values
export AWS_ACCOUNT_ID=<my-aws-account-id>
export AWS_REGION=<my-aws-region>
export CLUSTER_NAME=<my-eks-cluster-name>

# The Kubernetes namespace where the kubernetes-agent runner should run
export CLUSTER_RUNNER_NAMESPACE="humanitec-kubernetes-agent-runner-ns"

# The name of the Kubernetes service account to be assumed by the kubernetes-agent runner
export CLUSTER_RUNNER_SERVICEACCOUNT="humanitec-kubernetes-agent-runner"

# The Kubernetes namespace where the deployment jobs should run
export CLUSTER_RUNNER_JOB_NAMESPACE="humanitec-kubernetes-agent-runner-job-ns"

# The name of the Kubernetes service account to be assumed by the deployment jobs created by the kubernetes-agent runner
export CLUSTER_RUNNER_JOB_SERVICEACCOUNT="humanitec-kubernetes-agent-runner-job"

In a self-hosting setup only, add and adjust these additional values. Remove those that do not apply:

###
# Parameters for a self-hosting setup
###
# Override the Helm chart registry with your internal OCI registry
export KUBERNETES_AGENT_RUNNER_CHART_REPOSITORY="oci://myregistry.example.com/charts"
# Runner Helm chart version (optional)
export KUBERNETES_AGENT_RUNNER_CHART_VERSION="X.Y.Z"
# Your internal OCI registry for the runner image
export KUBERNETES_AGENT_RUNNER_IMAGE_REPOSITORY="myregistry.example.com/humanitec/humanitec-runner"
# Runner image tag (optional)
export KUBERNETES_AGENT_RUNNER_IMAGE_TAG="vX.Y.Z"
# Your internal Orchestrator API endpoint
export ORCHESTRATOR_API_URL="https://my-orchestrator-api.example.com"
  1. Obtain and output the EKS cluster’s OIDC issuer:
export CLUSTER_OIDC_ISSUER=$(aws eks describe-cluster --name $CLUSTER_NAME --region $AWS_REGION --query "cluster.identity.oidc.issuer" --output text | sed -e "s/^https:\/\///")

echo $CLUSTER_OIDC_ISSUER
  1. Create an IAM role for IRSA that allows the Kubernetes service account used by the runner jobs to assume it:
export RUNNER_ROLE_NAME=humanitec-kubernetes-agent-runner-${CLUSTER_NAME}

aws iam create-role \
  --role-name ${RUNNER_ROLE_NAME} \
  --assume-role-policy-document '{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Federated": "arn:aws:iam::'${AWS_ACCOUNT_ID}':oidc-provider/'${CLUSTER_OIDC_ISSUER}'"
        },
        "Action": "sts:AssumeRoleWithWebIdentity",
        "Condition": {
          "StringEquals": {
            "'${CLUSTER_OIDC_ISSUER}':aud": "sts.amazonaws.com",
            "'${CLUSTER_OIDC_ISSUER}':sub": "system:serviceaccount:'${CLUSTER_RUNNER_JOB_NAMESPACE}':'${CLUSTER_RUNNER_JOB_SERVICEACCOUNT}'"
          }
        }
      }
    ]
  }'
  1. Create and associate an IAM OIDC provider for your cluster:

Skip this step if you already have an IAM OIDC provider associated to your EKS cluster. To determine whether you do, refer to the AWS documentation .

eksctl utils associate-iam-oidc-provider --cluster $CLUSTER_NAME --approve

A common part for both TF and CLI setups is the need for a private/public key pair for the runner to authenticate against the Orchestrator:

openssl genpkey -algorithm ed25519 -out runner_private_key.pem
openssl pkey -in runner_private_key.pem -pubout -out runner_public_key.pem

  1. Define the runner resource using the public Humanitec module and the key pair to install the runner into the cluster and register it with the Orchestrator:
module "kubernetes_agent_runner" {
  source = "github.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner"
  # To pin a specific module release, use this notation:
  # source = "github.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner?ref=vX.Y.Z"

  humanitec_org_id             = local.humanitec_org
  runner_id                    = local.runner_id
  private_key_path             = "./runner_private_key.pem"
  public_key_path              = "./runner_public_key.pem"
  k8s_namespace                = local.cluster_runner_namespace
  k8s_service_account_name     = local.cluster_runner_serviceaccount
  k8s_job_namespace            = local.cluster_runner_job_namespace
  k8s_job_service_account_name = local.cluster_runner_job_serviceaccount

  # EKS IRSA configuration - link to the IAM role created above
  service_account_annotations = {
    "eks.amazonaws.com/role-arn" = aws_iam_role.agent_runner_irsa_role.arn
  }
}

# Provide the runner ID as an output
output "runner_id" {
  value = module.kubernetes_agent_runner.runner_id
}

Visit the module documentation  to see further configuration options and available releases.

In a self-hosting setup only, apply these additional changes to the module. Remove those that do not apply:

module "kubernetes_agent_runner" {
  # Use an internally hosted registry for the TF module. Append "?ref=vX.Y.Z" to pin a specific release
  source = "mygitservice.example.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner"
  #
  # Existing properties ...
  #
  ###
  # Parameters for a self-hosting setup
  # Remove those that do not apply
  ###
  # Use an internally hosted Helm chart registry
  kubernetes_agent_runner_chart_repository = local.kubernetes_agent_runner_chart_repository
  kubernetes_agent_runner_chart_version    = local.kubernetes_agent_runner_chart_version
  #
  # Use the API endpoint of a self-hosted Orchestrator
  extra_env_vars = [
    {
      name  = "REMOTE_URL"
      value = local.orchestrator_api_url
    }
  ]
  #
  # Use an internally hosted image registry
  kubernetes_agent_runner_image_repository = local.kubernetes_agent_runner_image_repository
  kubernetes_agent_runner_image_tag        = local.kubernetes_agent_runner_image_tag
  pod_template = jsonencode({
    metadata = {
      labels = {
        "app.kubernetes.io/name"    = "humanitec-runner"
        "app.kubernetes.io/version" = local.kubernetes_agent_runner_image_tag
      }
    }
    spec = {
      containers = [
        {
          name            = "main" # Use this exact value
          image           = "${local.kubernetes_agent_runner_image_repository}:${local.kubernetes_agent_runner_image_tag}"
          imagePullPolicy = "IfNotPresent"
        }
      ]
      # If your registry requires authentication, configure and reference an image pull secret
      # Otherwise remove the imagePullSecrets property
      imagePullSecrets = [
        {
          name = "regcred"
        }
      ]
    }
  })
}
  1. Assign any AWS permissions the runner will need to provision AWS resources to the IAM role. E.g. to enable the runner to manage Amazon RDS instances, attach a policy like this:
resource "aws_iam_role_policy_attachment" "agent_runner_manage_rds" {
  role       = aws_iam_role.agent_runner_irsa_role.name
  policy_arn = "arn:aws:iam::aws:policy/AmazonRDSFullAccess"
}

The exact permissions and whether you use built-in policies or custom policies is at your own discretion and depending on your resource requirements.

  1. Verify the kubectl context is set to your target cluster:
kubectl config current-context
  1. Initialize and apply the TF configuration:
Terraform
terraform init
terraform apply
OpenTofu
tofu init
tofu apply

  1. Verify the kubectl context is set to your target cluster:
kubectl config current-context
  1. Set values:
export HUMANITEC_ORG=<my-org>
export RUNNER_ID=kubernetes-agent-${CLUSTER_NAME}
  1. Create the namespace for the runner and a secret holding the runner private key:
kubectl create namespace ${CLUSTER_RUNNER_NAMESPACE}

kubectl create secret generic humanitec-kubernetes-agent-runner \
  -n ${CLUSTER_RUNNER_NAMESPACE} \
  --from-literal=private_key="$(cat runner_private_key.pem)"
  1. Create the namespace for the runner jobs:
kubectl create namespace ${CLUSTER_RUNNER_JOB_NAMESPACE}
  1. Install the runner Helm chart onto the cluster, providing required values:
helm install humanitec-kubernetes-agent-runner \
  oci://ghcr.io/humanitec/charts/humanitec-kubernetes-agent-runner \
  -n ${CLUSTER_RUNNER_NAMESPACE} \
  --set humanitec.orgId=${HUMANITEC_ORG} \
  --set humanitec.runnerId=${RUNNER_ID} \
  --set humanitec.existingSecret=humanitec-kubernetes-agent-runner \
  --set namespaceOverride=${CLUSTER_RUNNER_NAMESPACE} \
  --set serviceAccount.name=${CLUSTER_RUNNER_SERVICEACCOUNT} \
  --set jobsRbac.namespace=${CLUSTER_RUNNER_JOB_NAMESPACE} \
  --set jobsRbac.serviceAccountName=${CLUSTER_RUNNER_JOB_SERVICEACCOUNT} \
  --set "serviceAccount.annotations.eks\.amazonaws\.com/role-arn=$(aws iam get-role --role-name ${RUNNER_ROLE_NAME} | jq -r .Role.Arn)"

The value serviceAccount.annotations.eks... annotates the Kubernetes service account to leverage the EKS workload identity solution IRSA  for the runner to authenticate against AWS, using a role pre-configured as part of the initial base setup.

In a self-hosting setup only, append these settings to the helm install command. Remove those that do not apply:

  --version ${KUBERNETES_AGENT_RUNNER_CHART_VERSION} \
  --set image.repository=${KUBERNETES_AGENT_RUNNER_IMAGE_REPOSITORY} \
  --set image.tag=${KUBERNETES_AGENT_RUNNER_IMAGE_TAG} \
  --set-json humanitec.extraEnvVars='[{"name":"REMOTE_URL","value":"'${ORCHESTRATOR_API_URL}'"}]' \
  # ... use all other values as shown above
  1. Register the kubernetes-agent runner with the Orchestrator:
hctl create runner ${RUNNER_ID} \
  --set=runner_configuration="$(jq -nc --arg key "$(cat runner_public_key.pem)" '{"type": "kubernetes-agent","key":$key,"job":{"namespace":"'${CLUSTER_RUNNER_JOB_NAMESPACE}'","service_account":"'${CLUSTER_RUNNER_JOB_SERVICEACCOUNT}'"}}')" \
  --set=state_storage_configuration='{"type":"kubernetes","namespace":"'${CLUSTER_RUNNER_JOB_NAMESPACE}'"}'
  1. Assign any AWS permissions the runner will need to provision AWS resources to the IAM role. E.g. to enable the runner to manage Amazon RDS instances, attach a policy like this:
aws iam attach-role-policy \
  --role-name ${RUNNER_ROLE_NAME} \
  --policy-arn "arn:aws:iam::aws:policy/AmazonRDSFullAccess"

The exact permissions and whether you use built-in policies or custom policies is at your own discretion and depending on your resource requirements.

Your kubernetes-agent runner is now ready to be used. Continue by defining runner rules to assign the runner to environments and execute deployments.

GKE-specific setup

To have the kubernetes-agent runner in your GKE cluster, you need:

  • An existing GCP GKE cluster 
  • kubectl  installed locally and the current context set to the target cluster
  • The gcloud CLI  installed and authenticated with a principal having permission to manage IAM roles and policies in the GCP project of the GKE cluster
  • The gke-gcloud-auth-plugin CLI  installed and kubectl configured to use the plugin
  • The hctl CLI installed and authenticated against your Orchestrator organization
  • Depending on your choice of tooling:

The setup utilizes workload identity using a Google Service Account  for the GKE cluster to provide credentials to the runner for TF execution.

  1. Declare the required providers:
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 7"
    }
    # Providers needed to install the kubernetes-agent runner Helm chart and register it with the Orchestrator
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2"
    }
    platform-orchestrator = {
      source  = "humanitec/platform-orchestrator"
      version = "~> 2"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "~> 3"
    }
    local = {
      source  = "hashicorp/local"
      version = ">= 2.0"
    }
  }
}
  1. Set values according to your setup. Change the default values at your discretion:
locals {
  gcp_project_id = "<my-gcp-project-id>"
  gcp_region     = "<my-gcp-region>"
  cluster_name   = "<my-gke-cluster-name>"
  humanitec_org  = "<my-org>"
  runner_id      = "kubernetes-agent-${local.cluster_name}" # Adjust the ID at your discretion

  # The Kubernetes namespace where the kubernetes-agent runner should run
  cluster_runner_namespace = "humanitec-kubernetes-agent-runner-ns"

  # The name of the Kubernetes service account to be assumed by the kubernetes-agent runner
  cluster_runner_serviceaccount = "humanitec-kubernetes-agent-runner"

  # The Kubernetes namespace where the deployment jobs should run
  cluster_runner_job_namespace = "humanitec-kubernetes-agent-runner-job-ns"

  # The name of the Kubernetes service account to be assumed by the deployment jobs created by the kubernetes-agent runner
  cluster_runner_job_serviceaccount = "humanitec-kubernetes-agent-runner-job"
}

Use TF variables of your module instead of placing the raw values in the locals at your discretion.

In a self-hosting setup only, add and adjust these additional values. Remove those that do not apply:

locals {
  ###
  # Parameters for a self-hosting setup
  ###
  #
  # Your internal OCI registry for the runner Helm chart
  kubernetes_agent_runner_chart_repository = "oci://myregistry.example.com/charts"
  #
  # Runner Helm chart version (optional)
  kubernetes_agent_runner_chart_version = "X.Y.Z"
  #
  # Your internal OCI registry for the runner image
  kubernetes_agent_runner_image_repository = "myregistry.example.com/humanitec/humanitec-runner"
  #
  # Runner image tag (optional)
  kubernetes_agent_runner_image_tag = "vX.Y.Z"
  #
  # Your internal Orchestrator API endpoint
  orchestrator_api_url = "https://my-orchestrator-api.example.com"
}
  1. Create a data source from the existing cluster and extract required cluster properties:
# Data source for the existing EKS cluster
data "google_container_cluster" "cluster" {
  name     = local.cluster_name
  location = local.gcp_region
  project  = local.gcp_project_id
}

# Extract cluster properties
locals {
  cluster_ca_certificate = data.google_container_cluster.cluster.master_auth[0].cluster_ca_certificate
  k8s_cluster_endpoint   = data.google_container_cluster.cluster.endpoint
}
  1. Configure providers:
# GCP provider. Authentication taken from local "gcloud" CLI
provider "google" {
  project = local.gcp_project_id
  region  = local.gcp_region
}

# Platform Orchestrator provider. Authentication taken from local "hctl" CLI
provider "platform-orchestrator" {
  org_id = local.humanitec_org
}

# Kubernetes provider. Authentication taken from local "gke-gcloud-auth-plugin" CLI
provider "kubernetes" {
  host                   = "https://${local.k8s_cluster_endpoint}"
  cluster_ca_certificate = base64decode(local.cluster_ca_certificate)
  exec {
    api_version = "client.authentication.k8s.io/v1beta1"
    command     = "gke-gcloud-auth-plugin"
  }
}

# Helm provider. Kubernetes authentication taken from local "gke-gcloud-auth-plugin" CLI
provider "helm" {
  kubernetes = {
    host                   = "https://${local.k8s_cluster_endpoint}"
    cluster_ca_certificate = base64decode(local.cluster_ca_certificate)
    exec = {
      api_version = "client.authentication.k8s.io/v1beta1"
      command     = "gke-gcloud-auth-plugin"
    }
  }
}

If you are self-hosting the Helm chart in your own registry and that registry requires authentication, you need to configure credentials for Helm. There are several options.

variable "helm_repository_username" {
  type = string
}
variable "helm_repository_password" {
  type      = string
  sensitive = true
}
provider "helm" {
  # ...
  registries = [{
    url      = local.kubernetes_agent_runner_chart_repository
    username = var.helm_repository_username
    password = var.helm_repository_password
  }]
}

Pass the variable values using standard TF mechanisms, e.g. set the TF_VAR_helm_repository_username and TF_VAR_helm_repository_password environment variables on the client executing the TF apply.

  • Option 2: Use the helm CLI to login to the registry on the client executing the TF apply:
export HELM_REGISTRY_PASSWORD=<your-password>
echo $HELM_REGISTRY_PASSWORD | helm registry login myregistry.example.com -u admin --password-stdin

See the documentation on the helm registry login command  for supported ways to log in.

  1. Create a GCP IAM service account and allow the Kubernetes service account to impersonate it using GKE Workload Identity:
# This GCP service account will be used by the runner
resource "google_service_account" "humanitec_kubernetes_agent" {
  account_id   = "runner-${local.cluster_name}"
  display_name = "Humanitec Kubernetes agent runner on cluster ${local.cluster_name}"
}

resource "google_service_account_iam_member" "workload_identity_binding" {
  service_account_id = google_service_account.humanitec_kubernetes_agent.id
  role               = "roles/iam.workloadIdentityUser"
  member             = "serviceAccount:${local.gcp_project_id}.svc.id.goog[${local.cluster_runner_job_namespace}/${local.cluster_runner_job_serviceaccount}]"
}

  1. Set values
export GCP_PROJECT_ID=<my-gcp-project-id>
export CLUSTER_LOCATION=<my-cluster-region>
export CLUSTER_NAME=<my-cluster-name>

# The Kubernetes namespace where the kubernetes-agent runner should run
export CLUSTER_RUNNER_NAMESPACE="humanitec-kubernetes-agent-runner-ns"

# The name of the Kubernetes service account to be assumed by the kubernetes-agent runner
export CLUSTER_RUNNER_SERVICEACCOUNT="humanitec-kubernetes-agent-runner"

# The Kubernetes namespace where the deployment jobs should run
export CLUSTER_RUNNER_JOB_NAMESPACE="humanitec-kubernetes-agent-runner-job-ns"

# The name of the Kubernetes service account to be assumed by the deployment jobs created by the kubernetes-agent runner
export CLUSTER_RUNNER_JOB_SERVICEACCOUNT="humanitec-kubernetes-agent-runner-job"

In a self-hosting setup only, add and adjust these additional values. Remove those that do not apply:

###
# Parameters for a self-hosting setup
###
# Override the Helm chart registry with your internal OCI registry
export KUBERNETES_AGENT_RUNNER_CHART_REPOSITORY="oci://myregistry.example.com/charts"
# Runner Helm chart version (optional)
export KUBERNETES_AGENT_RUNNER_CHART_VERSION="X.Y.Z"
# Your internal OCI registry for the runner image
export KUBERNETES_AGENT_RUNNER_IMAGE_REPOSITORY="myregistry.example.com/humanitec/humanitec-runner"
# Runner image tag (optional)
export KUBERNETES_AGENT_RUNNER_IMAGE_TAG="vX.Y.Z"
# Your internal Orchestrator API endpoint
export ORCHESTRATOR_API_URL="https://my-orchestrator-api.example.com"
  1. Set the gcloud CLI to your project:
gcloud config set project $GCP_PROJECT_ID
  1. Create a Google Cloud service account:
export GCP_SERVICE_ACCOUNT_NAME=runner-${CLUSTER_NAME}

gcloud iam service-accounts create ${GCP_SERVICE_ACCOUNT_NAME} \
    --description="Used by Kubernetes Agent Runner" \
    --display-name="Humanitec Kubernetes agent runner on cluster ${CLUSTER_NAME}" \
    --project=${GCP_PROJECT_ID}
  1. Allow the Kubernetes service account to impersonate the Google Cloud service account using GKE Workload Identity:
gcloud iam service-accounts add-iam-policy-binding ${GCP_SERVICE_ACCOUNT_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com \
    --role="roles/iam.workloadIdentityUser" \
    --member="serviceAccount:${GCP_PROJECT_ID}.svc.id.goog[${CLUSTER_RUNNER_JOB_NAMESPACE}/${CLUSTER_RUNNER_JOB_SERVICEACCOUNT}]" \
    --project=${GCP_PROJECT_ID}

A common part for both TF and CLI setups is the need for a private/public key pair for the runner to authenticate against the Orchestrator:

openssl genpkey -algorithm ed25519 -out runner_private_key.pem
openssl pkey -in runner_private_key.pem -pubout -out runner_public_key.pem

  1. Define the runner resource using the public Humanitec module and the key pair to install the runner into the cluster and register it with the Orchestrator:
module "kubernetes_agent_runner" {
  source = "github.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner"
  # To pin a specific module release, use this notation:
  # source = "github.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner?ref=vX.Y.Z"

  humanitec_org_id             = local.humanitec_org
  runner_id                    = local.runner_id
  private_key_path             = "./runner_private_key.pem"
  public_key_path              = "./runner_public_key.pem"
  k8s_namespace                = local.cluster_runner_namespace
  k8s_service_account_name     = local.cluster_runner_serviceaccount
  k8s_job_namespace            = local.cluster_runner_job_namespace
  k8s_job_service_account_name = local.cluster_runner_job_serviceaccount

  # GKE Workload Identity configuration - link to the GCP service account created above
  service_account_annotations = {
    "iam.gke.io/gcp-service-account" = google_service_account.humanitec_kubernetes_agent.email
  }
}

# Provide the runner ID as an output
output "runner_id" {
  value = module.kubernetes_agent_runner.runner_id
}

Visit the module documentation  to see further configuration options and available releases.

In a self-hosting setup only, apply these additional changes to the module. Remove those that do not apply:

module "kubernetes_agent_runner" {
  # Use an internally hosted registry for the TF module. Append "?ref=vX.Y.Z" to pin a specific release
  source = "mygitservice.example.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner"
  #
  # Existing properties ...
  #
  ###
  # Parameters for a self-hosting setup
  # Remove those that do not apply
  ###
  # Use an internally hosted Helm chart registry
  kubernetes_agent_runner_chart_repository = local.kubernetes_agent_runner_chart_repository
  kubernetes_agent_runner_chart_version    = local.kubernetes_agent_runner_chart_version
  #
  # Use the API endpoint of a self-hosted Orchestrator
  extra_env_vars = [
    {
      name  = "REMOTE_URL"
      value = local.orchestrator_api_url
    }
  ]
  #
  # Use an internally hosted image registry
  kubernetes_agent_runner_image_repository = local.kubernetes_agent_runner_image_repository
  kubernetes_agent_runner_image_tag        = local.kubernetes_agent_runner_image_tag
  pod_template = jsonencode({
    metadata = {
      labels = {
        "app.kubernetes.io/name"    = "humanitec-runner"
        "app.kubernetes.io/version" = local.kubernetes_agent_runner_image_tag
      }
    }
    spec = {
      containers = [
        {
          name            = "main" # Use this exact value
          image           = "${local.kubernetes_agent_runner_image_repository}:${local.kubernetes_agent_runner_image_tag}"
          imagePullPolicy = "IfNotPresent"
        }
      ]
      # If your registry requires authentication, configure and reference an image pull secret
      # Otherwise remove the imagePullSecrets property
      imagePullSecrets = [
        {
          name = "regcred"
        }
      ]
    }
  })
}
  1. Assign any GCP permissions the runner will need to provision GCP resources to the IAM service account. E.g. to enable the runner to manage CloudSQL instances, attach a policy like this:
resource "google_project_iam_member" "agent_runner_cloudsql_admin" {
  project = local.gcp_project_id
  role    = "roles/cloudsql.admin"
  member  = "serviceAccount:${google_service_account.humanitec_kubernetes_agent.email}"
}
  1. Verify the kubectl context is set to your target cluster:
kubectl config current-context
  1. Initialize and apply the TF configuration:
Terraform
terraform init
terraform apply
OpenTofu
tofu init
tofu apply

  1. Verify the kubectl context is set to your target cluster:
kubectl config current-context
  1. Set values:
export HUMANITEC_ORG=<my-org>
export RUNNER_ID=kubernetes-agent-${CLUSTER_NAME}
  1. Create the namespace for the runner and a secret holding the runner private key:
kubectl create namespace ${CLUSTER_RUNNER_NAMESPACE}

kubectl create secret generic humanitec-kubernetes-agent-runner \
  -n ${CLUSTER_RUNNER_NAMESPACE} \
  --from-literal=private_key="$(cat runner_private_key.pem)"
  1. Create the namespace for the runner jobs:
kubectl create namespace ${CLUSTER_RUNNER_JOB_NAMESPACE}
  1. Install the runner Helm chart onto the cluster, providing required values:
helm install humanitec-kubernetes-agent-runner \
  oci://ghcr.io/humanitec/charts/humanitec-kubernetes-agent-runner \
  -n ${CLUSTER_RUNNER_NAMESPACE} \
  --set humanitec.orgId=${HUMANITEC_ORG} \
  --set humanitec.runnerId=${RUNNER_ID} \
  --set humanitec.existingSecret=humanitec-kubernetes-agent-runner \
  --set namespaceOverride=${CLUSTER_RUNNER_NAMESPACE} \
  --set serviceAccount.name=${CLUSTER_RUNNER_SERVICEACCOUNT} \
  --set jobsRbac.namespace=${CLUSTER_RUNNER_JOB_NAMESPACE} \
  --set jobsRbac.serviceAccountName=${CLUSTER_RUNNER_JOB_SERVICEACCOUNT} \
  --set "serviceAccount.annotations.iam\.gke\.io/gcp-service-account=${GCP_SERVICE_ACCOUNT_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com"

The value serviceAccount.annotations.iam.gke.io/gcp-service-account annotates the Kubernetes service account to leverage GKE Workload Identity for the runner to authenticate against GCP.

In a self-hosting setup only, append these settings to the helm install command. Remove those that do not apply:

  --version ${KUBERNETES_AGENT_RUNNER_CHART_VERSION} \
  --set image.repository=${KUBERNETES_AGENT_RUNNER_IMAGE_REPOSITORY} \
  --set image.tag=${KUBERNETES_AGENT_RUNNER_IMAGE_TAG} \
  --set-json humanitec.extraEnvVars='[{"name":"REMOTE_URL","value":"'${ORCHESTRATOR_API_URL}'"}]' \
  # ... use all other values as shown above
  1. Register the kubernetes-agent runner with the Orchestrator:
hctl create runner ${RUNNER_ID} \
  --set=runner_configuration="$(jq -nc --arg key "$(cat runner_public_key.pem)" '{"type": "kubernetes-agent","key":$key,"job":{"namespace":"'${CLUSTER_RUNNER_JOB_NAMESPACE}'","service_account":"'${CLUSTER_RUNNER_JOB_SERVICEACCOUNT}'"}}')" \
  --set=state_storage_configuration='{"type":"kubernetes","namespace":"'${CLUSTER_RUNNER_JOB_NAMESPACE}'"}'
  1. Assign any GCP permissions the runner will need to provision GCP resources to the IAM service account. E.g. to enable the runner to manage CloudSQL instances, attach a policy like this:
gcloud projects add-iam-policy-binding ${GCP_PROJECT_ID} \
  --member "serviceAccount:${GCP_SERVICE_ACCOUNT_NAME}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
  --role "roles/cloudsql.admin"

Your kubernetes-agent runner is now ready to be used. Continue by defining runner rules to assign the runner to environments and execute deployments.

AKS-specific setup

To have the kubernetes-agent runner in your AKS cluster, you need:

The setup utilizes Azure Workload Identity  for the AKS cluster to provide credentials to the runner for TF execution.

  1. Declare the required providers:
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4"
    }
    # Providers needed to install the kubernetes-agent runner Helm chart and register it with the Orchestrator
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2"
    }
    platform-orchestrator = {
      source  = "humanitec/platform-orchestrator"
      version = "~> 2"
    }
    helm = {
      source  = "hashicorp/helm"
      version = "~> 3"
    }
    local = {
      source  = "hashicorp/local"
      version = ">= 2.0"
    }
  }
}
  1. Set values according to your setup. Change the default values at your discretion:
locals {
  azure_subscription_id = "<my-azure-subscription-id>"
  azure_resource_group  = "<my-azure-resource-group>"
  cluster_name          = "<my-aks-cluster-name>"
  humanitec_org         = "<my-org>"
  runner_id             = "kubernetes-agent-${local.cluster_name}" # Adjust the ID at your discretion

  # The Kubernetes namespace where the kubernetes-agent runner should run
  cluster_runner_namespace = "humanitec-kubernetes-agent-runner-ns"

  # The name of the Kubernetes service account to be assumed by the kubernetes-agent runner
  cluster_runner_serviceaccount = "humanitec-kubernetes-agent-runner"

  # The Kubernetes namespace where the deployment jobs should run
  cluster_runner_job_namespace = "humanitec-kubernetes-agent-runner-job-ns"

  # The name of the Kubernetes service account to be assumed by the deployment jobs created by the kubernetes-agent runner
  cluster_runner_job_serviceaccount = "humanitec-kubernetes-agent-runner-job"
}

Use TF variables of your module instead of placing the raw values in the locals at your discretion.

In a self-hosting setup only, add and adjust these additional values. Remove those that do not apply:

locals {
  ###
  # Parameters for a self-hosting setup
  ###
  #
  # Your internal OCI registry for the runner Helm chart
  kubernetes_agent_runner_chart_repository = "oci://myregistry.example.com/charts"
  #
  # Runner Helm chart version (optional)
  kubernetes_agent_runner_chart_version = "X.Y.Z"
  #
  # Your internal OCI registry for the runner image
  kubernetes_agent_runner_image_repository = "myregistry.example.com/humanitec/humanitec-runner"
  #
  # Runner image tag (optional)
  kubernetes_agent_runner_image_tag = "vX.Y.Z"
  #
  # Your internal Orchestrator API endpoint
  orchestrator_api_url = "https://my-orchestrator-api.example.com"
}
  1. Create a data source from the existing cluster and extract required cluster properties:
# Data source for the existing AKS cluster
data "azurerm_kubernetes_cluster" "cluster" {
  name                = local.cluster_name
  resource_group_name = local.azure_resource_group
}

# Extract cluster properties
locals {
  cluster_oidc_issuer_url = data.azurerm_kubernetes_cluster.cluster.oidc_issuer_url
}
  1. Configure providers:
# Azure provider. Authentication taken from local "az" CLI
provider "azurerm" {
  subscription_id = local.azure_subscription_id
  features {}
}

# Platform Orchestrator provider. Authentication taken from local "hctl" CLI
provider "platform-orchestrator" {
  org_id = local.humanitec_org
}

# Kubernetes provider. Authentication taken from local kubeconfig context
# Before running TF, set your context: az aks get-credentials --resource-group <rg> --name <cluster>
# Check the provider documentation for alternative ways of authentication
provider "kubernetes" {
  config_path    = "~/.kube/config" # Adjust if needed or remove if default works
  config_context = local.cluster_name
}

# Helm provider. Kubernetes authentication taken from local kubeconfig context
# Check the provider documentation for alternative ways of authentication
provider "helm" {
  kubernetes = {
    config_path    = "~/.kube/config" # Adjust if needed or remove if default works
    config_context = local.cluster_name
  }
}

If you are self-hosting the Helm chart in your own registry and that registry requires authentication, you need to configure credentials for Helm. There are several options.

variable "helm_repository_username" {
  type = string
}
variable "helm_repository_password" {
  type      = string
  sensitive = true
}
provider "helm" {
  # ...
  registries = [{
    url      = local.kubernetes_agent_runner_chart_repository
    username = var.helm_repository_username
    password = var.helm_repository_password
  }]
}

Pass the variable values using standard TF mechanisms, e.g. set the TF_VAR_helm_repository_username and TF_VAR_helm_repository_password environment variables on the client executing the TF apply.

  • Option 2: Use the helm CLI to login to the registry on the client executing the TF apply:
export HELM_REGISTRY_PASSWORD=<your-password>
echo $HELM_REGISTRY_PASSWORD | helm registry login myregistry.example.com -u admin --password-stdin

See the documentation on the helm registry login command  for supported ways to log in.

  1. Create a User Assigned Managed Identity and a Federated Identity Credential to enable Azure Workload Identity for the runner jobs:
# This managed identity will be assumed by the runner jobs
resource "azurerm_user_assigned_identity" "humanitec_kubernetes_agent" {
  name                = "humanitec-kubernetes-agent-runner-${local.cluster_name}"
  resource_group_name = local.azure_resource_group
  location            = data.azurerm_kubernetes_cluster.cluster.location
}

# Allow the Kubernetes service account used by the runner jobs to assume the managed identity
resource "azurerm_federated_identity_credential" "humanitec_kubernetes_agent" {
  name                = "humanitec-kubernetes-agent-runner"
  parent_id           = azurerm_user_assigned_identity.humanitec_kubernetes_agent.id
  issuer              = local.cluster_oidc_issuer_url
  subject             = "system:serviceaccount:${local.cluster_runner_job_namespace}:${local.cluster_runner_job_serviceaccount}"
  audience            = ["api://AzureADTokenExchange"]
}

  1. Set values
export AZURE_SUBSCRIPTION_ID=<my-azure-subscription-id>
export AZURE_RESOURCE_GROUP=<my-azure-resource-group>
export CLUSTER_NAME=<my-aks-cluster-name>

# The Kubernetes namespace where the kubernetes-agent runner should run
export CLUSTER_RUNNER_NAMESPACE="humanitec-kubernetes-agent-runner-ns"

# The name of the Kubernetes service account to be assumed by the kubernetes-agent runner
export CLUSTER_RUNNER_SERVICEACCOUNT="humanitec-kubernetes-agent-runner"

# The Kubernetes namespace where the deployment jobs should run
export CLUSTER_RUNNER_JOB_NAMESPACE="humanitec-kubernetes-agent-runner-job-ns"

# The name of the Kubernetes service account to be assumed by the deployment jobs created by the kubernetes-agent runner
export CLUSTER_RUNNER_JOB_SERVICEACCOUNT="humanitec-kubernetes-agent-runner-job"

In a self-hosting setup only, add and adjust these additional values. Remove those that do not apply:

###
# Parameters for a self-hosting setup
###
# Override the Helm chart registry with your internal OCI registry
export KUBERNETES_AGENT_RUNNER_CHART_REPOSITORY="oci://myregistry.example.com/charts"
# Runner Helm chart version (optional)
export KUBERNETES_AGENT_RUNNER_CHART_VERSION="X.Y.Z"
# Your internal OCI registry for the runner image
export KUBERNETES_AGENT_RUNNER_IMAGE_REPOSITORY="myregistry.example.com/humanitec/humanitec-runner"
# Runner image tag (optional)
export KUBERNETES_AGENT_RUNNER_IMAGE_TAG="vX.Y.Z"
# Your internal Orchestrator API endpoint
export ORCHESTRATOR_API_URL="https://my-orchestrator-api.example.com"
  1. Obtain and output the AKS cluster’s OIDC issuer URL:
export CLUSTER_OIDC_ISSUER=$(az aks show \
  --name ${CLUSTER_NAME} \
  --resource-group ${AZURE_RESOURCE_GROUP} \
  --query "oidcIssuerProfile.issuerUrl" \
  -o tsv)

echo $CLUSTER_OIDC_ISSUER
  1. Create a User Assigned Managed Identity for the runner jobs:
export MANAGED_IDENTITY_NAME=humanitec-kubernetes-agent-runner-${CLUSTER_NAME}

az identity create \
  --name ${MANAGED_IDENTITY_NAME} \
  --resource-group ${AZURE_RESOURCE_GROUP} \
  --subscription ${AZURE_SUBSCRIPTION_ID}
  1. Create a Federated Identity Credential to allow the Kubernetes service account used by the runner jobs to assume the managed identity:
az identity federated-credential create \
  --name humanitec-kubernetes-agent-runner \
  --identity-name ${MANAGED_IDENTITY_NAME} \
  --resource-group ${AZURE_RESOURCE_GROUP} \
  --issuer ${CLUSTER_OIDC_ISSUER} \
  --subject "system:serviceaccount:${CLUSTER_RUNNER_JOB_NAMESPACE}:${CLUSTER_RUNNER_JOB_SERVICEACCOUNT}" \
  --audiences api://AzureADTokenExchange

A common part for both TF and CLI setups is the need for a private/public key pair for the runner to authenticate against the Orchestrator:

openssl genpkey -algorithm ed25519 -out runner_private_key.pem
openssl pkey -in runner_private_key.pem -pubout -out runner_public_key.pem

  1. Define the runner resource using the public Humanitec module and the key pair to install the runner into the cluster and register it with the Orchestrator:
module "kubernetes_agent_runner" {
  source = "github.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner"
  # To pin a specific module release, use this notation:
  # source = "github.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner?ref=vX.Y.Z"

  humanitec_org_id             = local.humanitec_org
  runner_id                    = local.runner_id
  private_key_path             = "./runner_private_key.pem"
  public_key_path              = "./runner_public_key.pem"
  k8s_namespace                = local.cluster_runner_namespace
  k8s_service_account_name     = local.cluster_runner_serviceaccount
  k8s_job_namespace            = local.cluster_runner_job_namespace
  k8s_job_service_account_name = local.cluster_runner_job_serviceaccount

  # Azure Workload Identity configuration for runner job pods:
  # - the service account annotation identifies the managed identity to use
  # - the pod label triggers token injection by the AKS Workload Identity webhook
  service_account_annotations = {
    "azure.workload.identity/client-id" = azurerm_user_assigned_identity.humanitec_kubernetes_agent.client_id
  }
  pod_template = jsonencode({
    metadata = {
      labels = {
        "azure.workload.identity/use" = "true"
      }
    }
  })
}

# Provide the runner ID as an output
output "runner_id" {
  value = module.kubernetes_agent_runner.runner_id
}

Visit the module documentation  to see further configuration options and available releases.

In a self-hosting setup only, apply these additional changes to the module. Remove those that do not apply:

module "kubernetes_agent_runner" {
  # Use an internally hosted registry for the TF module. Append "?ref=vX.Y.Z" to pin a specific release
  source = "mygitservice.example.com/humanitec-tf-modules/kubernetes-agent-orchestrator-runner"
  #
  # Existing properties ...
  #
  ###
  # Parameters for a self-hosting setup
  # Remove those that do not apply
  ###
  # Use an internally hosted Helm chart registry
  kubernetes_agent_runner_chart_repository = local.kubernetes_agent_runner_chart_repository
  kubernetes_agent_runner_chart_version    = local.kubernetes_agent_runner_chart_version
  #
  # Use the API endpoint of a self-hosted Orchestrator
  extra_env_vars = [
    {
      name  = "REMOTE_URL"
      value = local.orchestrator_api_url
    }
  ]
  #
  # Use an internally hosted image registry
  kubernetes_agent_runner_image_repository = local.kubernetes_agent_runner_image_repository
  kubernetes_agent_runner_image_tag        = local.kubernetes_agent_runner_image_tag
  pod_template = jsonencode({
    metadata = {
      labels = {
        "app.kubernetes.io/name"    = "humanitec-runner"
        "app.kubernetes.io/version" = local.kubernetes_agent_runner_image_tag
      }
    }
    spec = {
      containers = [
        {
          name            = "main" # Use this exact value
          image           = "${local.kubernetes_agent_runner_image_repository}:${local.kubernetes_agent_runner_image_tag}"
          imagePullPolicy = "IfNotPresent"
        }
      ]
      # If your registry requires authentication, configure and reference an image pull secret
      # Otherwise remove the imagePullSecrets property
      imagePullSecrets = [
        {
          name = "regcred"
        }
      ]
    }
  })
}
  1. Assign any Azure permissions the runner will need to provision Azure resources to the managed identity. E.g. to enable the runner to manage Azure SQL servers, attach a role assignment like this:
resource "azurerm_role_assignment" "agent_runner_sql_contributor" {
  scope                = "/subscriptions/${local.azure_subscription_id}/resourceGroups/${local.azure_resource_group}"
  role_definition_name = "SQL Server Contributor"
  principal_id         = azurerm_user_assigned_identity.humanitec_kubernetes_agent.principal_id
}

The exact permissions and scope is at your own discretion and depending on your resource requirements.

  1. Verify the kubectl context is set to your target cluster:
az aks get-credentials --resource-group <my-azure-resource-group> --name <my-aks-cluster-name>
kubectl config current-context
  1. Initialize and apply the TF configuration:
Terraform
terraform init
terraform apply
OpenTofu
tofu init
tofu apply

  1. Verify the kubectl context is set to your target cluster:
az aks get-credentials --resource-group ${AZURE_RESOURCE_GROUP} --name ${CLUSTER_NAME}
kubectl config current-context
  1. Set values:
export HUMANITEC_ORG=<my-org>
export RUNNER_ID=kubernetes-agent-${CLUSTER_NAME}
  1. Create the namespace for the runner and a secret holding the runner private key:
kubectl create namespace ${CLUSTER_RUNNER_NAMESPACE}

kubectl create secret generic humanitec-kubernetes-agent-runner \
  -n ${CLUSTER_RUNNER_NAMESPACE} \
  --from-literal=private_key="$(cat runner_private_key.pem)"
  1. Create the namespace for the runner jobs:
kubectl create namespace ${CLUSTER_RUNNER_JOB_NAMESPACE}
  1. Install the runner Helm chart onto the cluster, providing required values:
export MANAGED_IDENTITY_CLIENT_ID=$(az identity show \
  --name ${MANAGED_IDENTITY_NAME} \
  --resource-group ${AZURE_RESOURCE_GROUP} \
  --query clientId -o tsv)

helm install humanitec-kubernetes-agent-runner \
  oci://ghcr.io/humanitec/charts/humanitec-kubernetes-agent-runner \
  -n ${CLUSTER_RUNNER_NAMESPACE} \
  --set humanitec.orgId=${HUMANITEC_ORG} \
  --set humanitec.runnerId=${RUNNER_ID} \
  --set humanitec.existingSecret=humanitec-kubernetes-agent-runner \
  --set namespaceOverride=${CLUSTER_RUNNER_NAMESPACE} \
  --set serviceAccount.name=${CLUSTER_RUNNER_SERVICEACCOUNT} \
  --set jobsRbac.namespace=${CLUSTER_RUNNER_JOB_NAMESPACE} \
  --set jobsRbac.serviceAccountName=${CLUSTER_RUNNER_JOB_SERVICEACCOUNT} \
  --set "serviceAccount.annotations.azure\.workload\.identity/client-id=${MANAGED_IDENTITY_CLIENT_ID}"

The value serviceAccount.annotations.azure.workload.identity/client-id annotates the Kubernetes service account to identify the managed identity to use for Azure Workload Identity.

In a self-hosting setup only, append these settings to the helm install command. Remove those that do not apply:

  --version ${KUBERNETES_AGENT_RUNNER_CHART_VERSION} \
  --set image.repository=${KUBERNETES_AGENT_RUNNER_IMAGE_REPOSITORY} \
  --set image.tag=${KUBERNETES_AGENT_RUNNER_IMAGE_TAG} \
  --set-json humanitec.extraEnvVars='[{"name":"REMOTE_URL","value":"'${ORCHESTRATOR_API_URL}'"}]' \
  # ... use all other values as shown above
  1. Register the kubernetes-agent runner with the Orchestrator, configuring the pod label required by Azure Workload Identity:
hctl create runner ${RUNNER_ID} \
  --set=runner_configuration="$(jq -nc --arg key "$(cat runner_public_key.pem)" '{"type": "kubernetes-agent","key":$key,"job":{"namespace":"'${CLUSTER_RUNNER_JOB_NAMESPACE}'","service_account":"'${CLUSTER_RUNNER_JOB_SERVICEACCOUNT}'","pod_template":{"metadata":{"labels":{"azure.workload.identity/use":"true"}}}}}')" \
  --set=state_storage_configuration='{"type":"kubernetes","namespace":"'${CLUSTER_RUNNER_JOB_NAMESPACE}'"}'

The pod_template.metadata.labels value triggers token injection by the AKS Workload Identity webhook.

  1. Assign any Azure permissions the runner will need to provision Azure resources to the managed identity. E.g. to enable the runner to manage Azure SQL servers, attach a role assignment like this:
export MANAGED_IDENTITY_PRINCIPAL_ID=$(az identity show \
  --name ${MANAGED_IDENTITY_NAME} \
  --resource-group ${AZURE_RESOURCE_GROUP} \
  --query principalId -o tsv)

az role assignment create \
  --assignee ${MANAGED_IDENTITY_PRINCIPAL_ID} \
  --role "SQL Server Contributor" \
  --scope /subscriptions/${AZURE_SUBSCRIPTION_ID}/resourceGroups/${AZURE_RESOURCE_GROUP}

The exact permissions and scope is at your own discretion and depending on your resource requirements.

Your kubernetes-agent runner is now ready to be used. Continue by defining runner rules to assign the runner to environments and execute deployments.

Assign runner permissions

The kubernetes-agent runner will spawn Kubernetes Jobs and corresponding Pods in the Kubernetes namespace and using the Kubernetes service account you configured as part of the runner setup.

The runner Pods will execute the TF code for a deployment, containing the required TF providers. Each TF provider has its own way of receiving authentication configuration for gaining permissions.

For example, the hashicorp/aws provider  may use container credentials for EKS workload identity through annotating the Kubernetes service account for the runner Pods. The setup instructions for EKS therefore include an appropriate annotation.

Consult your Kubernetes runtime environment and TF providers to determine the required configuration. You have these options with the kubernetes-agent runner:

  • Providing Kubernetes service account annotations
  • Providing environment variables
  • Modifying the runner Pod template in any other custom way to make data available to the runner

The kubernetes-agent runner TF module  shows examples for all these cases.

Setting sensitive environment variables

The runner_configuration.job.pod_template field contains a Kubernetes pod template you can set to extend the runtime configuration of the runner. The pod template expects a structure of pod spec with a container named main. You can set secret environment variables by referencing existing secrets within the same target namespace of the runner pod. For example, if you want to mount the value of the key field within a secret named my-secret to the environment variable TF_EXAMPLE, you can set the pod template as the following:

  runner_configuration = {
    job = {
      pod_template = jsonencode({
        spec = {
          containers = [
            {
              name = "main"
              env = [
                {
                  name = "TF_EXAMPLE"
                  valueFrom = {
                    secretKeyRef = {
                      name = "my-secret"
                      key  = "key"
                    }
                  }
                }
              ]
            }
          ]
        }
      })
    }
  }

runner_configuration:
  job:
    pod_template:
      spec:
        containers:
          - name: main
            env:
              name: TF_EXAMPLE
              valueFrom:
                secretKeyRef:
                  name: my-secret
                  key: key

You can also use this mechanism to inject environment variables into TF variables by using the TF_VAR_ prefix. This naming is the standard mechanism to inject environment variables into TF variables in Terraform  and OpenTofu .

The service account used by the runner must have permissions to get the secret.

Environment variables that are not secret or sensitive can be set directly in the env structure.

Top