{"id":1891,"date":"2025-04-22T09:00:00","date_gmt":"2025-04-22T06:00:00","guid":{"rendered":"https:\/\/upcloud.com\/global\/us\/resources\/tutorials\/deploy-lightning-fast-github-actions-runners-on-upclouds-managed-kubernetes-part-3\/"},"modified":"2025-04-22T09:00:00","modified_gmt":"2025-04-22T06:00:00","slug":"deploy-lightning-fast-github-actions-runners-on-upclouds-managed-kubernetes-part-3","status":"publish","type":"tutorial","link":"https:\/\/upcloud.com\/global\/resources\/tutorials\/deploy-lightning-fast-github-actions-runners-on-upclouds-managed-kubernetes-part-3\/","title":{"rendered":"Deploy Lightning-Fast GitHub Actions Runners on UpCloud\u2019s Managed Kubernetes: Part 3"},"content":{"rendered":"\n\n\n<p class=\"wp-block-paragraph\">Welcome back to the four-part series on supercharging your CI\/CD pipelines by deploying lightning-fast GitHub Actions Runners on UpCloud\u2019s Managed Kubernetes! <a href=\"https:\/\/upcloud.com\/global\/resources\/tutorials\/supercharge-your-ci-cd-deploy-lightning-fast-github-actions-runners-on-upclouds-managed-kubernetes-part-2\">In Part 2<\/a>, we looked at advanced configurations, including customizing runner deployments, implementing network policies for enhanced security, and setting up autoscaling to optimize costs.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In this installment, Part 3, we turn our attention to maintaining and troubleshooting your self-hosted runner environment. From monitoring runner health to diagnosing and resolving common issues like registration errors, resource constraints, and job execution failures, we will look at the tools and techniques needed to ensure smooth operations. Let\u2019s dive right in!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Monitoring Runners<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Monitoring self-hosted GitHub Actions runners is important to ensure the reliability, performance, and scalability of your CI\/CD pipelines. By tracking a few key metrics, you can identify bottlenecks, optimize resource utilization, and promptly address issues before they impact your development workflow.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Luckily, GitHub Actions Runner ships with out-of-the-box support for key performance and usage metrics. In this section, you will learn how to enable them and which ones to track.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Exposing Metrics From Your GitHub Actions Runner Controller<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To enable Prometheus to collect metrics from the Actions Runner Controller, first update the controller\u2019s values file to enable the metrics:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">metrics:\n  controllerManagerAddr: \":8080\"\n  listenerAddr: \":8080\"\n  listenerEndpoint: \"\/metrics\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You will need to do a fresh install of the controller to apply these changes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next, you will also need to enable the default listener metrics in the scalesets you deploy in your cluster. To do that, create a values file for the scaleset and save the following contents in it:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">listenerMetrics:\n  counters:\n    gha_started_jobs_total:\n      labels:\n        [\"repository\", \"organization\", \"enterprise\", \"job_name\", \"event_name\"]\n    gha_completed_jobs_total:\n      labels:\n        [\n          \"repository\",\n          \"organization\",\n          \"enterprise\",\n          \"job_name\",\n          \"event_name\",\n          \"job_result\",\n        ]\n  gauges:\n    gha_assigned_jobs:\n      labels: [\"name\", \"namespace\", \"repository\", \"organization\", \"enterprise\"]\n    gha_running_jobs:\n      labels: [\"name\", \"namespace\", \"repository\", \"organization\", \"enterprise\"]\n    gha_registered_runners:\n      labels: [\"name\", \"namespace\", \"repository\", \"organization\", \"enterprise\"]\n    gha_busy_runners:\n      labels: [\"name\", \"namespace\", \"repository\", \"organization\", \"enterprise\"]\n    gha_min_runners:\n      labels: [\"name\", \"namespace\", \"repository\", \"organization\", \"enterprise\"]\n    gha_max_runners:\n      labels: [\"name\", \"namespace\", \"repository\", \"organization\", \"enterprise\"]\n    gha_desired_runners:\n      labels: [\"name\", \"namespace\", \"repository\", \"organization\", \"enterprise\"]\n    gha_idle_runners:\n      labels: [\"name\", \"namespace\", \"repository\", \"organization\", \"enterprise\"]\n  histograms:\n    gha_job_startup_duration_seconds:\n      labels:\n        [\"repository\", \"organization\", \"enterprise\", \"job_name\", \"event_name\"]\n      buckets:\n        [\n          0.01,\n          0.05,\n          0.1,\n          0.5,\n          1.0,\n          2.0,\n          3.0,\n          4.0,\n          5.0,\n          6.0,\n          7.0,\n          8.0,\n          9.0,\n          10.0,\n          12.0,\n          15.0,\n          18.0,\n          20.0,\n          25.0,\n          30.0,\n          40.0,\n          50.0,\n          60.0,\n          70.0,\n          80.0,\n          90.0,\n          100.0,\n          110.0,\n          120.0,\n          150.0,\n          180.0,\n          210.0,\n          240.0,\n          300.0,\n          360.0,\n          420.0,\n          480.0,\n          540.0,\n          600.0,\n          900.0,\n          1200.0,\n          1800.0,\n          2400.0,\n          3000.0,\n          3600.0,\n        ]\n    gha_job_execution_duration_seconds:\n      labels:\n        [\n          \"repository\",\n          \"organization\",\n          \"enterprise\",\n          \"job_name\",\n          \"event_name\",\n          \"job_result\",\n        ]\n      buckets:\n        [\n          0.01,\n          0.05,\n          0.1,\n          0.5,\n          1.0,\n          2.0,\n          3.0,\n          4.0,\n          5.0,\n          6.0,\n          7.0,\n          8.0,\n          9.0,\n          10.0,\n          12.0,\n          15.0,\n          18.0,\n          20.0,\n          25.0,\n          30.0,\n          40.0,\n          50.0,\n          60.0,\n          70.0,\n          80.0,\n          90.0,\n          100.0,\n          110.0,\n          120.0,\n          150.0,\n          180.0,\n          210.0,\n          240.0,\n          300.0,\n          360.0,\n          420.0,\n          480.0,\n          540.0,\n          600.0,\n          900.0,\n          1200.0,\n          1800.0,\n          2400.0,\n          3000.0,\n          3600.0,\n        ]\n\ntemplate:\n    spec:\n      containers:\n      - name: runner\n        image: ghcr.io\/actions\/actions-runner:latest\n        command: [\"\/home\/runner\/run.sh\"]<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Now, you can verify that the metrics are being exposed by setting up a port forward and accessing them in your browser:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl port-forward pod\/&lt;your-controller-pod-name&gt; 8080:8080 -n arc-systems<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You can get your controller pod\u2019s name by running <code>kubectl get pods -n arc-systems<\/code> and looking for a pod that has the word \u201ccontroller\u201d in its name:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">\u279c  gh-actions-runner-p3 kubectl get pods -n arc-systems\nNAME                                     READY   STATUS    RESTARTS   AGE\narc-gha-rs-controller-5d96468674-5hctf   1\/1     Running   0          157m\narc-runner-set-754b578d-listener         1\/1     Running   0          154m\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Once you have set up the port forwarding, try accessing the controller metrics now at <code>http:\/\/localhost:8080\/metrics<\/code>. You should see something like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-150-1024x594.png\" alt=\"-\" class=\"wp-image-49870\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Similarly, you can try port-forwarding the listener pod and viewing the metrics that it exposes:<br><\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img decoding=\"async\" src=\"https:\/\/upcloud.com\/media\/image-151-1024x594.png\" alt=\"-\" class=\"wp-image-49871\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Understanding GitHub Actions Runner metrics<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The configuration you have used above enabled and exposed all metrics supported by the Actions Runner Controller. The GitHub docs describe these <a href=\"https:\/\/docs.github.com\/en\/actions\/hosting-your-own-runners\/managing-self-hosted-runners-with-actions-runner-controller\/deploying-runner-scale-sets-with-actions-runner-controller#available-metrics-for-arc\" target=\"_blank\" rel=\"noopener\">metrics here<\/a>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you were planning to build a performance monitoring dashboard, here are some metrics that would come in handy:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Metric<\/strong><\/td><td><strong>Description<\/strong><\/td><td><strong>Purpose<\/strong><\/td><\/tr><tr><td>gha_running_jobs<\/td><td>Number of jobs currently running or queued<\/td><td>Monitors workload and queue depth<\/td><\/tr><tr><td>gha_assigned_jobs<\/td><td>Number of jobs assigned to the runner scale set<\/td><td>Measures how many jobs were accepted for processing<\/td><\/tr><tr><td>gha_started_jobs_total<\/td><td>Cumulative number of jobs started<\/td><td>Tracks overall job volume<\/td><\/tr><tr><td>gha_completed_jobs_total<\/td><td>Cumulative number of jobs completed<\/td><td>Ensures all jobs complete successfully<\/td><\/tr><tr><td>gha_job_startup_duration_seconds<\/td><td>Time to start a job (histogram)<\/td><td>Highlights scheduling\/startup latency<\/td><\/tr><tr><td>gha_job_execution_duration_seconds<\/td><td>Job execution time (histogram)<\/td><td>Helps monitor execution duration trends and outliers<\/td><\/tr><tr><td>gha_busy_runners<\/td><td>Number of runners currently executing a job<\/td><td>Shows how much capacity is being used<\/td><\/tr><tr><td>gha_idle_runners<\/td><td>Number of idle runners<\/td><td>Identifies underutilized capacity<\/td><\/tr><tr><td>gha_desired_runners<\/td><td>Target number of runners to scale to<\/td><td>Verifies if autoscaling is working correctly<\/td><\/tr><tr><td>gha_min_runners, gha_max_runners<\/td><td>Scale bounds set for the scale set<\/td><td>Helps correlate with autoscaling behaviors<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">For a health monitoring dashboard, the following metrics would be useful:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Metric<\/strong><\/td><td><strong>Description<\/strong><\/td><td><strong>Purpose<\/strong><\/td><\/tr><tr><td>gha_controller_pending_ephemeral_runners<\/td><td>Number of runners stuck in pending state<\/td><td>Detects issues in provisioning runners<\/td><\/tr><tr><td>gha_controller_running_ephemeral_runners<\/td><td>Runners that are active<\/td><td>Baseline to compare expected runner load<\/td><\/tr><tr><td>gha_controller_failed_ephemeral_runners<\/td><td>Runners that failed to start<\/td><td>Alerts on provisioning or execution issues<\/td><\/tr><tr><td>gha_controller_running_listeners<\/td><td>Number of active listener pods<\/td><td>Verifies if listener components are healthy<\/td><\/tr><tr><td>gha_registered_runners<\/td><td>Runners registered with the scale set<\/td><td>Tracks overall runner availability<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Some other insights you can draw from these metrics include:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Metric<\/strong><\/td><td><strong>Description<\/strong><\/td><td><strong>Purpose<\/strong><\/td><\/tr><tr><td>Job Success Rate<\/td><td>gha_completed_jobs_total \/ gha_started_jobs_total<\/td><td>Helps monitor CI stability and test flakiness<\/td><\/tr><tr><td>Runner Utilization Rate<\/td><td>gha_busy_runners \/ gha_registered_runners<\/td><td>Determines scaling efficiency<\/td><\/tr><tr><td>Startup Latency Trends<\/td><td>Percentiles from gha_job_startup_duration_seconds<\/td><td>Identify if jobs are waiting too long to start (e.g., due to image pull or resource contention)<\/td><\/tr><tr><td>Execution Time Trends<\/td><td>Histogram from gha_job_execution_duration_seconds<\/td><td>Benchmark CI job complexity and highlight anomalies<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Also, all counter metrics (like gha_started_jobs_total) reset when the pod restarts. So you must ensure you\u2019re using Prometheus with a persistent backend (like <a href=\"https:\/\/thanos.io\/\" target=\"_blank\" rel=\"noopener\">Thanos<\/a> or <a href=\"https:\/\/cortexmetrics.io\/\" target=\"_blank\" rel=\"noopener\">Cortex<\/a>) or reset-aware alerting. And you can also use histogram metrics (like durations) to create SLOs (e.g., \u201c90% of jobs start within 10s\u201d).<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Understanding Runner Logs<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Now that you have an active monitoring setup in place, it is time to look at the tools to use when you need to troubleshoot your controllers. Analyzing runner logs can come in quite handy for identifying and diagnosing issues in your self-hosted runners. Logs provide valuable insights into runner operations, errors, and warnings, allowing you to resolve problems quickly and maintain seamless CI\/CD workflows.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Below are step-by-step instructions for accessing and interpreting runner logs within your Kubernetes environment:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Identify Your Runner Pods<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">First, identify the runner pods deployed by the Actions Runner Controller (ARC). Use the following command to list runner pods in your ARC namespace (typically arc-runners):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl get pods -n arc-runners<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This command lists all runner pods along with their current status:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">NAME                                           READY   STATUS    RESTARTS   AGE\nrunner-deployment-example-runner-abcde-12345   2\/2     Running   0          3h<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><em>It is possible to receive an empty (\u201cno resources found\u201d) response for this command. This can happen if the runner has finished executing a job and has been terminated by the scaleset. In such situations, you might need to add a temporary sleep 3600 or similar step in your GitHub Actions workflow file and trigger the workflow again to make the runner pod stay alive for longer while you can try identifying it and accessing its logs.<\/em><\/p>\n<\/blockquote>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Access Runner Logs Using kubectl<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Once you have retrieved the name of the runner pod, run the following command to access its logs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl logs &lt;runner-pod-name&gt; -n arc-runners<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Analyze Runner Logs for Common Errors<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Now, review the logs carefully, looking specifically for patterns like:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Authentication errors (e.g., invalid tokens)<\/li>\n\n\n\n<li>Connectivity issues (e.g., inability to reach GitHub)<\/li>\n\n\n\n<li>Job execution failures (e.g., script errors, missing dependencies)<\/li>\n\n\n\n<li>Resource exhaustion messages (e.g., CPU or memory limits exceeded)<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">One important check you can do is to search for the \u201cJob message\u201d, which is an object sent by the remote GitHub Actions service with the details of the job which is currently executing on the runner. The presence of this object in the logs rules out any connectivity\/authentication issues between the remote GitHub service and your runner scaleset. The details of this object can help you in determining missing\/incorrect references or values mentioned in the workflow or one of the runner configuration files.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Filtering and Tail Logs<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To continuously monitor logs or filter for specific messages, use:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl logs &lt;runner-pod-name&gt; -n arc-runners --tail=50 -f<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This command displays the latest 50 log entries and continues streaming new log entries in real time.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Saving Logs for Further Analysis<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If you need to save logs for troubleshooting or reporting, redirect logs to a file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl logs &lt;runner-pod-name&gt; -n arc-runners &gt; runner-logs.txt<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This allows you to examine logs offline or share them with your team for collaborative debugging.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Debugging Runner Registration Issues<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Runner registration issues can prevent your GitHub Actions self-hosted runners from joining your workflow, causing disruptions in your CI\/CD pipeline. These problems often appear as runners stuck in a \u201cPending\u201d state, authentication errors, or namespace conflicts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Typical symptoms indicating runner registration issues include:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Pods remain stuck in Pending or CrashLoopBackOff status.<\/li>\n\n\n\n<li>Log messages showing authentication failures or invalid tokens.<\/li>\n\n\n\n<li>Errors related to API access or namespace conflicts.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">For example, you might encounter logs like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">Runner registration failed: Authentication token expired or invalid.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">To fix this, you should start by verifying your GitHub Personal Access Token. Check that your GitHub Personal Access Token (PAT) used for runner registration is valid and has the necessary permissions:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Go to your GitHub repository or organization: Settings \u2192 Actions \u2192 Runners<\/li>\n\n\n\n<li>Confirm your token is still valid and hasn\u2019t expired or been revoked.<\/li>\n<\/ol>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Confirm your token is still valid and hasn\u2019t expired or been revoked.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">If necessary, generate a new token and update your ARC secret accordingly.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Next, check namespace usage and permissions. You need to ensure the namespace you\u2019re using for the Actions Runner Controller (ARC) is dedicated exclusively to ARC deployments.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Run the following command to list the deployments and pods in your ARC namespaces (<code>arc-systems<\/code> and <code>arc-runners<\/code>) and ensure that no non-ARC resources are deployed here:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl get deployments,pods -n &lt;your-arc-namespace&gt;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Also, validate that the roles and rolebindings created in these namespaces are accurate and allow the controller to register runners. By default, permissions are correctly configured when installing ARC via Helm. Manual changes can sometimes cause permission-related errors.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Finally, if nothing works, you can directly debug at the pod level by executing commands within the runner pod:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl exec -it &lt;runner-pod-name&gt; -n &lt;your-arc-namespace&gt; -- \/bin\/bash<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Once inside, you can try performing additional checks, such as verifying connectivity to GitHub or inspecting the runner configuration directly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Scale Set Controller Troubleshooting<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The Scale Set Controller in Actions Runner Controller (ARC) is responsible for managing the number of runner pods based on job demand. When something goes wrong, you might notice symptoms like:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Runner pods not scaling up when jobs are queued.<\/li>\n\n\n\n<li>Pods being created but failing to start.<\/li>\n\n\n\n<li>Stale or idle pods not terminating as expected.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">These issues often stem from misconfigurations or resource constraints. Here\u2019s how to troubleshoot them effectively.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Check the Scale Set Controller Pod<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">First, ensure that the scale set controller pod is healthy:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl get pods -n arc-systems<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Look for a pod named something like actions-runner-controller-xxxxx. If it\u2019s in a <code>CrashLoopBackOff <\/code>or <code>Error<\/code> state, inspect its logs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl logs &lt;controller-pod-name&gt; -n arc-systems<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Common causes could include:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Invalid or missing GitHub credentials<\/li>\n\n\n\n<li>GitHub API rate limiting<\/li>\n\n\n\n<li>Misconfigured Helm values (e.g., missing enterprise, organization, or repository fields)<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Watch Runner Pods in Real Time<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Another way to approach this could be to look at runner scaling activity in real time while triggering a workflow. To do that, run the following command:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl get runners -n arc-runners -w<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This helps you confirm whether runners are being created in response to incoming jobs and whether they\u2019re terminating after execution.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Check Helm Values for Scaling Parameters<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If no runners are spinning up, revisit your Helm config:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Is <code>minRunners<\/code> set to 0 or too low?<\/li>\n\n\n\n<li>Is <code>maxRunners <\/code>high enough to meet demand?<\/li>\n\n\n\n<li>Are GitHub org\/repo\/labels correctly configured?<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Updating your Helm release with correct values may resolve silent scaling issues.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Look for Cluster Resource Issues<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Finally, make sure your cluster has capacity. Even if ARC wants to schedule pods, they\u2019ll stay in Pending if nodes are out of resources:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl describe pod &lt;runner-pod-name&gt; -n arc-runners<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Look for \u201cInsufficient memory\u201d or \u201cInsufficient cpu\u201d messages.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Addressing Resource Issues<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Even when your GitHub Actions runners are correctly configured, they may fail to start or behave unpredictably due to resource constraints in your Kubernetes cluster. These issues can surface as pods stuck in <code>Pending<\/code>, runners terminating prematurely, or jobs taking longer than expected.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Identify Resource Constraints<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To find out if a runner pod failed due to lack of resources, describe the pod:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl describe pod &lt;runner-pod-name&gt; -n arc-runners<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Look for messages like:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">0\/3 nodes are available: insufficient memory.<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This indicates the cluster doesn\u2019t have enough available CPU or memory to schedule new runner pods.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Review and Adjust Resource Requests<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">As you\u2019ve seen in the last part, you can define resource requests and limits for your runners in the scaleset\u2019s values file. If your runners frequently hit memory or CPU limits, consider increasing these values. Conversely, if jobs are lightweight and overprovisioned, reducing the resource requests can improve overall cluster utilization.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Scale the Cluster or Add Node Pools<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If your runners are correctly configured but still can\u2019t schedule due to capacity, you may need to add more nodes to your cluster or set up <a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/cluster-autoscaler\/\">auto-scaling<\/a> for your node pools.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Handling Job Execution Failures<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Job execution failures can occur even when runners are healthy and scaling properly. These failures typically arise from issues within the GitHub Actions workflows themselves or from environment-related problems inside the runner pod.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Check Workflow Logs in GitHub<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Start by reviewing the job logs directly in the GitHub Actions UI. Common causes of failure include:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Syntax errors in the workflow YAML<\/li>\n\n\n\n<li>Missing or incorrect secrets<\/li>\n\n\n\n<li>Broken shell scripts or failing test commands<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">These errors will be visible in the step-by-step job output.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>Inspect Runner Pod Logs<\/strong><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If the failure seems environment-related, inspect the runner pod logs:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\">kubectl logs &lt;runner-pod-name&gt; -n arc-runners<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Look for errors like missing binaries, filesystem write errors, or permission issues.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Some general tips to reduce such failures include:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Use retry logic in workflows (workflow-level <code>continue-on-error<\/code> or <code>max-attempts <\/code>configuration)<\/li>\n\n\n\n<li>Add timeouts to prevent stuck jobs<\/li>\n\n\n\n<li>Validate workflows with <a href=\"https:\/\/github.com\/nektos\/act\" target=\"_blank\" rel=\"noopener\">act<\/a> locally before pushing<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Useful Commands and Quick Reference<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here\u2019s a handy collection of commands to help you troubleshoot, monitor, and manage your GitHub Actions runners on UpCloud\u2019s Managed Kubernetes:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>View ARC pods and their status<\/strong>: <code>kubectl get pods -n arc-systems<\/code><\/li>\n\n\n\n<li><strong>Watch runner pods in real time<\/strong>: <code>kubectl get pods -n arc-runners -w<\/code><\/li>\n\n\n\n<li><strong>Describe a runner pod to debug issues<\/strong>: <code>kubectl describe pod &lt;runner-pod-name&gt; -n arc-runners<\/code><\/li>\n\n\n\n<li><strong>View logs for a runner pod<\/strong>: <code>kubectl logs &lt;runner-pod-name&gt; -n arc-runners<\/code><\/li>\n\n\n\n<li><strong>Exec into a runner pod shell<\/strong>: <code>kubectl exec -it &lt;runner-pod-name&gt; -n arc-runners -- \/bin\/bash<\/code><\/li>\n\n\n\n<li><strong>Restart the ARC deployment<\/strong>: <code>kubectl rollout restart deployment actions-runner-controller -n arc-systems<\/code><\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>Conclusion<\/strong><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In this third part of the series, we focused on keeping your GitHub Actions runners healthy, observable, and resilient. You learned how to monitor runners with Prometheus and Grafana, how to inspect logs and debug registration issues, and how to troubleshoot common problems with scaling, resources, and job execution. With these tools and techniques, you should now feel confident about detecting issues early and ensuring the smooth operation of your self-hosted CI\/CD pipelines.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But deploying fast and reliable runners is only part of the story. In Part 4, we\u2019ll shift our attention to security best practices\u2014covering everything from managing secrets securely and implementing RBAC to setting up audit-friendly logging and update strategies that keep your pipeline compliant and protected. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/upcloud.com\/global\/resources\/tutorials\/supercharge-your-ci-cd-deploy-lightning-fast-github-actions-runners-on-upclouds-managed-kubernetes-part-4\">Continue on with the part 4 to harden your GitHub Actions runners with best security practices.<\/a><\/p>\n","protected":false},"author":82,"featured_media":49878,"comment_status":"open","ping_status":"closed","template":"","community-category":[223],"class_list":["post-1891","tutorial","type-tutorial","status-publish","has-post-thumbnail","hentry"],"acf":[],"_links":{"self":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/1891","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial"}],"about":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/types\/tutorial"}],"author":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/users\/82"}],"replies":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/comments?post=1891"}],"version-history":[{"count":0,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/1891\/revisions"}],"wp:attachment":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/media?parent=1891"}],"wp:term":[{"taxonomy":"community-category","embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/community-category?post=1891"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}