Install Shared ARC Controller
1. 🧭 Scope Legend
These markers separate the cluster-shared controller from the organization, repository, and deployment-branch resources created on later pages.
2. ✅ Why This Classification Makes Sense
Your request is the correct architecture. Install exactly one ARC controller in this shared Kubernetes cluster. Do not install separate controllers for FP, Shelvera, or future products. The controller watches the cluster and reconciles runner scale sets created later for different GitHub organizations, repositories, and deployment environments.
ONE SHARED CLUSTER
cicd-ac-k8s-api.aspireclan.com / 192.168.8.200
3 control planes
4 development workers
4 QA workers
4 production workers
ONE SHARED ARC CONTROLLER
Namespace: arc-systems
Release: arc
Chart: gha-runner-scale-set-controller 0.14.2
REPEATED LATER
Per GitHub organization/product:
GitHub App
organization/environment namespaces
Harbor project and organization credentials
Per private repository:
runner scale set
repository GitHub URL
repository-level Harbor settings
Per deployment branch/environment:
dev, qa, or prod namespace mapping
node selector
taint toleration
min/max runner capacityCOMMON · NO CHANGE NEEDED
Shared Kubernetes cluster
Helm CLI on cicd-ac-k8s-cp-01
ARC controller namespace and release
ARC controller chart version
ARC controller replicas, resources, logging, and placement
ARC CRDs and controller RBAC
CHANGE PER GITHUB ORG
GitHub App and installation
GitHub App Kubernetes Secret
Harbor project
Harbor runtime and CI robot accounts
organization working folder
CHANGE PER REPOSITORY
GitHub repository URL
runner scale-set name and Helm release
repository runner values file
repository-level GitHub Actions secrets and variables
CHANGE PER DEPLOYMENT BRANCH
dev, qa, or prod runner namespace
environment node selector
environment taint toleration
runner capacity and deployment target credentials3. 🧱 Shared ARC Controller Inputs
4. 🏢 Future Tenant Identity Preview
arc-runners-<<APP_SHORT_FORM>>-<<ENVIRONMENT>><<APP_SHORT_FORM>>-arc-ghapp-secret<<APP_SHORT_FORM>>-ci-cd<<SERVICE_NAME>>-<<ENVIRONMENT>>-archttps://github.com/<<GITHUB_ORGANIZATION>>/<<SERVICE_NAME>>environment=<<ENVIRONMENT>>, workload=github-runnerenvironment=<<ENVIRONMENT>>:NoSchedule5. 📊 From-Scratch Sequence Checkpoint
FROM-SCRATCH SEQUENCE CHECKPOINT
Required before this page
Load-balancer VM, HAProxy, Keepalived, and API VIP configured
Three control-plane nodes Ready
Four development workers Ready and isolated
Four QA workers Ready and isolated
Four production workers Ready and isolated
15 Kubernetes nodes total
Kubernetes API /readyz returns ok
GitHub and GHCR outbound access available
Implemented by this page
Pinned Helm installation on the first control plane
Shared ARC controller values and Ansible role
Shared ARC controller Helm release in arc-systems
Two controller replicas on control-plane nodes
ARC actions.github.com CRDs
Implemented by later pages
GitHub organization onboarding
Repository runner scale sets
Deployment-branch listener and runner mappings6. 📁 Files Created by This Page
helm/common/arc-controller/values.yaml
ansible/roles/arc-controller/defaults/main.yml
ansible/roles/arc-controller/tasks/main.yml
ansible/playbooks/shared-k8s/09-install-arc-controller.yml
.github/workflows/ansible-install-arc-controller.ymlPreserve without replacement
terraform/**
ansible/inventories/shared-k8s/**
ansible/roles/common/**
ansible/roles/containerd/**
ansible/roles/kubernetes-common/**
ansible/roles/kubernetes-control-plane/**
ansible/roles/kubernetes-worker/**
ansible/playbooks/shared-k8s/01-common-baseline.yml
ansible/playbooks/shared-k8s/02-configure-load-balancer.yml
ansible/playbooks/shared-k8s/03-prepare-kubernetes-nodes.yml
ansible/playbooks/shared-k8s/04-bootstrap-first-control-plane.yml
ansible/playbooks/shared-k8s/05-join-control-planes.yml
ansible/playbooks/shared-k8s/06-install-cni.yml
ansible/playbooks/shared-k8s/07-join-dev-workers.yml
ansible/playbooks/shared-k8s/07-join-qa-workers.yml
ansible/playbooks/shared-k8s/07-join-prod-workers.yml
ansible/playbooks/shared-k8s/08-label-and-taint-workers.yml
ansible/playbooks/shared-k8s/08-label-and-taint-qa-workers.yml
ansible/playbooks/shared-k8s/08-label-and-taint-prod-workers.yml
.github/workflows/ansible-configure-control-planes.yml
.github/workflows/ansible-configure-dev-workers.yml
.github/workflows/ansible-configure-qa-workers.yml
.github/workflows/ansible-configure-prod-workers.yml
.github/workflows/ansible-configure-shared-k8s.yml
.github/workflows/ansible-configure-load-balancer.yml7. 🌿 Create the Feature Branch
feature/install-shared-arc-controller
↓ merge or push
dev
↓ a dev push runs validation only
dev → prod promotion
↓ the merge creates a prod push
prod
↓ validation, installation, and reconciliation
Pull requests may still be used for code review, but no pull_request workflow trigger is configured.
Not used by this infrastructure execution flow:
local, qa, maincd D:\code\ASPIRECLAN-LLC-Org\ac-cicd-infra
git switch dev
git pull --ff-only origin dev
git switch -c feature/install-shared-arc-controller8. 🔎 Verify the Prerequisite Cluster and Internet Egress
ssh \
-i ~/.ssh/id_ed25519_ansible \
-o IdentitiesOnly=yes \
acllc@192.168.8.202 \
'sudo bash -s' <<'REMOTE'
set -euo pipefail
export KUBECONFIG=/etc/kubernetes/admin.conf
echo "=== API READINESS ==="
kubectl get --raw=/readyz
echo
echo "=== ALL NODES ==="
kubectl get nodes -o wide
echo
echo "=== ENVIRONMENT WORKERS ==="
kubectl get nodes -L environment,workload
echo
echo "=== EXACT NODE COUNT ==="
node_count="$(kubectl get nodes -o name | wc -l)"
echo "Kubernetes node count: ${node_count}"
test "${node_count}" -eq 15
echo
echo "=== WAIT FOR EVERY NODE TO BE READY ==="
kubectl wait \
--for=condition=Ready \
node \
--all \
--timeout=2m
echo
echo "=== EXISTING ARC RESOURCES ==="
if [ -x /usr/local/bin/helm ]; then
/usr/local/bin/helm list -A
else
echo "Helm is not installed yet. This is expected before the prod installation."
fi
kubectl get crd | grep actions.github.com || true
kubectl get pods -A | grep -Ei 'arc|runner|listener' || true
REMOTEssh \
-i ~/.ssh/id_ed25519_ansible \
-o IdentitiesOnly=yes \
acllc@192.168.8.202 \
'set -e
getent hosts ghcr.io
getent hosts api.github.com
curl -fsSI --connect-timeout 10 https://ghcr.io/v2/ >/dev/null
curl -fsSI --connect-timeout 10 https://api.github.com/ >/dev/null
echo "GitHub and GHCR egress checks passed."'9. 📝 Create the Shared Controller Values File
Create helm/common/arc-controller/values.yaml:
replicaCount: 2
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
nodeSelector:
node-role.kubernetes.io/control-plane: ""
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
# Prefer placing the two controller replicas on different control-plane VMs.
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app.kubernetes.io/part-of: gha-rs-controller
priorityClassName: system-cluster-critical
flags:
logLevel: "info"
logFormat: "json"10. ⚙️ Create the ARC Controller Role Defaults
Create ansible/roles/arc-controller/defaults/main.yml:
---
arc_admin_hostname: "cicd-ac-k8s-cp-01"
arc_admin_ip: "192.168.8.202"
arc_kubeconfig: "/etc/kubernetes/admin.conf"
arc_helm_version: "v3.21.0"
arc_helm_archive_sha256: "0093eb572e3d2380f094df162ddb525e219249de88957afe24cfbb19632acd36"
arc_helm_archive_url: >-
https://get.helm.sh/helm-{{ arc_helm_version }}-linux-amd64.tar.gz
arc_helm_archive_path: "/tmp/helm-{{ arc_helm_version }}-linux-amd64.tar.gz"
arc_helm_extract_directory: "/tmp/helm-{{ arc_helm_version }}-linux-amd64"
arc_helm_binary_path: "/usr/local/bin/helm"
arc_controller_namespace: "arc-systems"
arc_controller_release_name: "arc"
arc_controller_chart_version: "0.14.2"
arc_controller_chart: >-
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller
arc_controller_replica_count: 2
arc_controller_values_source: >-
{{ playbook_dir }}/../../../helm/common/arc-controller/values.yaml
arc_controller_work_directory: "/etc/ac-cicd-infra/arc-controller"
arc_controller_values_remote: >-
{{ arc_controller_work_directory }}/values.yaml11. 🛠️ Create the ARC Controller Role Tasks
Create ansible/roles/arc-controller/tasks/main.yml:
---
- name: Confirm the ARC controller is being managed from the first control plane
ansible.builtin.assert:
that:
- inventory_hostname == groups['first_control_plane'][0]
- inventory_hostname == arc_admin_hostname
- ansible_facts["default_ipv4"]["address"] == arc_admin_ip
fail_msg: >-
The ARC controller playbook must run from the approved first control plane.
- name: Force refresh the APT package cache before installing ARC prerequisites
ansible.builtin.apt:
update_cache: true
register: arc_apt_cache_refresh
retries: 5
delay: 15
until: arc_apt_cache_refresh is succeeded
- name: Install ARC controller administration prerequisites
ansible.builtin.apt:
name:
- ca-certificates
- curl
- gzip
- tar
state: present
register: arc_prerequisite_install
retries: 3
delay: 10
until: arc_prerequisite_install is succeeded
- name: Check the currently installed Helm version
ansible.builtin.command:
cmd: "{{ arc_helm_binary_path }} version --short"
register: arc_existing_helm_version
changed_when: false
failed_when: false
- name: Download the approved Helm archive
ansible.builtin.get_url:
url: "{{ arc_helm_archive_url }}"
dest: "{{ arc_helm_archive_path }}"
checksum: "sha256:{{ arc_helm_archive_sha256 }}"
owner: root
group: root
mode: "0644"
when: arc_helm_version not in (arc_existing_helm_version.stdout | default(''))
- name: Recreate the Helm extraction directory
ansible.builtin.file:
path: "{{ arc_helm_extract_directory }}"
state: absent
when: arc_helm_version not in (arc_existing_helm_version.stdout | default(''))
- name: Create the Helm extraction directory
ansible.builtin.file:
path: "{{ arc_helm_extract_directory }}"
state: directory
owner: root
group: root
mode: "0755"
when: arc_helm_version not in (arc_existing_helm_version.stdout | default(''))
- name: Extract the approved Helm archive
ansible.builtin.unarchive:
src: "{{ arc_helm_archive_path }}"
dest: "{{ arc_helm_extract_directory }}"
remote_src: true
when: arc_helm_version not in (arc_existing_helm_version.stdout | default(''))
- name: Install the approved Helm binary
ansible.builtin.copy:
src: "{{ arc_helm_extract_directory }}/linux-amd64/helm"
dest: "{{ arc_helm_binary_path }}"
remote_src: true
owner: root
group: root
mode: "0755"
when: arc_helm_version not in (arc_existing_helm_version.stdout | default(''))
- name: Verify the installed Helm version
ansible.builtin.command:
cmd: "{{ arc_helm_binary_path }} version --short"
register: arc_installed_helm_version
changed_when: false
failed_when: arc_helm_version not in arc_installed_helm_version.stdout
- name: Create the ARC controller working directory
ansible.builtin.file:
path: "{{ arc_controller_work_directory }}"
state: directory
owner: root
group: root
mode: "0755"
- name: Copy the Git-managed ARC controller values file
ansible.builtin.copy:
src: "{{ arc_controller_values_source }}"
dest: "{{ arc_controller_values_remote }}"
owner: root
group: root
mode: "0644"
- name: Confirm the Kubernetes API is ready before installing ARC
ansible.builtin.command:
cmd: >-
kubectl
--kubeconfig={{ arc_kubeconfig }}
get --raw=/readyz
register: arc_api_ready
changed_when: false
retries: 12
delay: 10
until: arc_api_ready.stdout | trim == "ok"
- name: Render the pinned ARC controller chart before installation
ansible.builtin.shell:
executable: /bin/bash
cmd: |
set -euo pipefail
{{ arc_helm_binary_path }} template \
{{ arc_controller_release_name }} \
{{ arc_controller_chart }} \
--namespace {{ arc_controller_namespace }} \
--version {{ arc_controller_chart_version }} \
--values {{ arc_controller_values_remote }} \
--kubeconfig {{ arc_kubeconfig }} \
> {{ arc_controller_work_directory }}/rendered.yaml
changed_when: false
- name: Install or reconcile the shared ARC controller
ansible.builtin.command:
argv:
- "{{ arc_helm_binary_path }}"
- upgrade
- --install
- "{{ arc_controller_release_name }}"
- "{{ arc_controller_chart }}"
- --namespace
- "{{ arc_controller_namespace }}"
- --create-namespace
- --version
- "{{ arc_controller_chart_version }}"
- --values
- "{{ arc_controller_values_remote }}"
- --kubeconfig
- "{{ arc_kubeconfig }}"
- --atomic
- --wait
- --timeout
- 10m
- --history-max
- "10"
register: arc_controller_helm_apply
changed_when: >-
'has been upgraded' in arc_controller_helm_apply.stdout or
'has been installed' in arc_controller_helm_apply.stdout
- name: Wait for the ARC controller deployment to become available
ansible.builtin.command:
cmd: >-
kubectl
--kubeconfig={{ arc_kubeconfig }}
wait
--namespace {{ arc_controller_namespace }}
--for=condition=Available
deployment
--selector=app.kubernetes.io/part-of=gha-rs-controller
--timeout=10m
changed_when: false
- name: Confirm the approved ARC controller replica count is available
ansible.builtin.shell:
executable: /bin/bash
cmd: |
set -euo pipefail
available="$({{ arc_helm_binary_path }} status \
{{ arc_controller_release_name }} \
--namespace {{ arc_controller_namespace }} \
--kubeconfig {{ arc_kubeconfig }} >/dev/null && \
kubectl \
--kubeconfig={{ arc_kubeconfig }} \
get deployment \
--namespace {{ arc_controller_namespace }} \
--selector=app.kubernetes.io/part-of=gha-rs-controller \
--output=jsonpath='{.items[0].status.availableReplicas}')"
test "${available:-0}" = "{{ arc_controller_replica_count }}"
register: arc_controller_replica_check
changed_when: false
retries: 30
delay: 10
until: arc_controller_replica_check.rc == 0
- name: Verify ARC custom resource definitions
ansible.builtin.shell:
executable: /bin/bash
cmd: |
set -euo pipefail
kubectl \
--kubeconfig={{ arc_kubeconfig }} \
get crd \
--output=name |
grep -E 'actions\.github\.com$'
register: arc_crd_check
changed_when: false
- name: Display the installed ARC controller release
ansible.builtin.command:
cmd: >-
{{ arc_helm_binary_path }} status
{{ arc_controller_release_name }}
--namespace {{ arc_controller_namespace }}
--kubeconfig {{ arc_kubeconfig }}
register: arc_controller_status
changed_when: false
- name: Print the ARC controller release status
ansible.builtin.debug:
var: arc_controller_status.stdout_lines12. 📘 Create the Shared ARC Controller Playbook
Create ansible/playbooks/shared-k8s/09-install-arc-controller.yml:
---
- name: Install the shared Actions Runner Controller
hosts: first_control_plane
become: true
gather_facts: true
roles:
- role: arc-controller13. 🔄 Create the ARC Controller GitHub Actions Workflow
Create .github/workflows/ansible-install-arc-controller.yml:
name: Ansible Install - Shared ARC Controller
on:
push:
branches:
- dev
- prod
paths:
- "helm/common/arc-controller/**"
- "ansible/roles/arc-controller/**"
- "ansible/playbooks/shared-k8s/09-install-arc-controller.yml"
- ".github/workflows/ansible-install-arc-controller.yml"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: shared-k8s-ansible
cancel-in-progress: false
env:
ANSIBLE_CONFIG: ${{ github.workspace }}/ansible/ansible.cfg
jobs:
validate:
name: Validate shared ARC controller configuration
runs-on:
- self-hosted
- Linux
- X64
- prod
- terraform
- deploy
- ac-cicd-infra
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Verify required files
shell: bash
run: |
set -euo pipefail
required_files=(
"helm/common/arc-controller/values.yaml"
"ansible/roles/arc-controller/defaults/main.yml"
"ansible/roles/arc-controller/tasks/main.yml"
"ansible/playbooks/shared-k8s/09-install-arc-controller.yml"
)
for file in "${required_files[@]}"; do
if [ ! -f "${file}" ]; then
echo "ERROR: Missing ${file}"
exit 1
fi
done
- name: Parse the ARC controller values YAML
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import yaml
path = Path("helm/common/arc-controller/values.yaml")
values = yaml.safe_load(path.read_text(encoding="utf-8"))
assert values["replicaCount"] >= 2
assert values["nodeSelector"]["node-role.kubernetes.io/control-plane"] == ""
assert values["priorityClassName"] == "system-cluster-critical"
assert values["flags"]["logFormat"] == "json"
print("ARC controller values YAML is valid.")
PY
- name: Validate the ARC playbook target
working-directory: ansible
shell: bash
run: |
set -euo pipefail
output="$(
ansible-playbook \
-i inventories/shared-k8s/hosts.ini \
playbooks/shared-k8s/09-install-arc-controller.yml \
--list-hosts
)"
printf '%s\n' "${output}"
grep -Fq "cicd-ac-k8s-cp-01" <<< "${output}"
if grep -Eq 'cicd-ac-k8s-(dev|qa|prod)-wk-' <<< "${output}"; then
echo "ERROR: ARC controller playbook resolves to a worker node."
exit 1
fi
- name: Syntax-check the ARC controller playbook
working-directory: ansible
shell: bash
run: |
set -euo pipefail
ansible-playbook \
-i inventories/shared-k8s/hosts.ini \
playbooks/shared-k8s/09-install-arc-controller.yml \
--syntax-check
configure:
name: Install or reconcile the shared ARC controller
needs:
- validate
if: >-
(github.event_name == 'push' && github.ref_name == 'prod') ||
(github.event_name == 'workflow_dispatch' && github.ref_name == 'prod')
environment:
name: shared-k8s
runs-on:
- self-hosted
- Linux
- X64
- prod
- terraform
- deploy
- ac-cicd-infra
timeout-minutes: 90
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Verify the production branch
shell: bash
run: |
set -euo pipefail
if [ "${GITHUB_REF_NAME}" != "prod" ]; then
echo "ERROR: Shared ARC controller changes are permitted only from prod."
exit 1
fi
- name: Prepare the existing Ansible SSH key
shell: bash
run: |
set -euo pipefail
KEY_PATH="${HOME}/.ssh/id_ed25519_ansible"
if [ ! -f "${KEY_PATH}" ]; then
echo "ERROR: Missing Ansible key: ${KEY_PATH}"
exit 1
fi
chmod 600 "${KEY_PATH}"
echo "ANSIBLE_PRIVATE_KEY_FILE=${KEY_PATH}" >> "${GITHUB_ENV}"
- name: Refresh the first-control-plane SSH host key
shell: bash
run: |
set -euo pipefail
mkdir -p "${HOME}/.ssh"
chmod 700 "${HOME}/.ssh"
touch "${HOME}/.ssh/known_hosts"
chmod 600 "${HOME}/.ssh/known_hosts"
ssh-keygen \
-f "${HOME}/.ssh/known_hosts" \
-R "192.168.8.202" || true
captured=false
for attempt in $(seq 1 30); do
if ssh-keyscan \
-T 5 \
-H "192.168.8.202" \
>> "${HOME}/.ssh/known_hosts" 2>/dev/null
then
echo "SSH host key captured for 192.168.8.202."
captured=true
break
fi
echo "Waiting for SSH on 192.168.8.202 (attempt ${attempt}/30)..."
sleep 10
done
if [ "${captured}" != "true" ]; then
echo "ERROR: Unable to capture the SSH host key for 192.168.8.202."
exit 1
fi
- name: Prepare the Ansible remote temporary directory
shell: bash
run: |
set -euo pipefail
ssh \
-i "${ANSIBLE_PRIVATE_KEY_FILE}" \
-o IdentitiesOnly=yes \
-o BatchMode=yes \
"acllc@192.168.8.202" \
'sudo install -d -m 0700 -o acllc -g acllc /var/tmp/ansible-acllc'
- name: Verify Ansible connectivity
working-directory: ansible
shell: bash
run: |
set -euo pipefail
ansible \
-i inventories/shared-k8s/hosts.ini \
first_control_plane \
--private-key "${ANSIBLE_PRIVATE_KEY_FILE}" \
-m ansible.builtin.ping
- name: Confirm the complete Kubernetes cluster is healthy
working-directory: ansible
shell: bash
run: |
set -euo pipefail
ansible \
-i inventories/shared-k8s/hosts.ini \
first_control_plane \
--private-key "${ANSIBLE_PRIVATE_KEY_FILE}" \
--become \
--extra-vars ansible_shell_executable=/bin/bash \
-m ansible.builtin.shell \
-a '
set -euo pipefail
export KUBECONFIG=/etc/kubernetes/admin.conf
node_count="$(kubectl get nodes -o name | wc -l)"
echo "Kubernetes node count: ${node_count}"
test "${node_count}" -eq 15
kubectl wait \
--for=condition=Ready \
node \
--all \
--timeout=2m
kubectl get --raw=/readyz | grep -Fx ok
'
- name: Install or reconcile the shared ARC controller
working-directory: ansible
shell: bash
run: |
set -euo pipefail
ansible-playbook \
-i inventories/shared-k8s/hosts.ini \
--private-key "${ANSIBLE_PRIVATE_KEY_FILE}" \
playbooks/shared-k8s/09-install-arc-controller.yml
- name: Verify the shared ARC controller
working-directory: ansible
shell: bash
run: |
set -euo pipefail
ansible \
-i inventories/shared-k8s/hosts.ini \
first_control_plane \
--private-key "${ANSIBLE_PRIVATE_KEY_FILE}" \
--become \
--extra-vars ansible_shell_executable=/bin/bash \
-m ansible.builtin.shell \
-a '
set -euo pipefail
export KUBECONFIG=/etc/kubernetes/admin.conf
test -x /usr/local/bin/helm
/usr/local/bin/helm status arc -n arc-systems
kubectl get namespace arc-systems
controller_deployment="$(
kubectl get deployment \
-n arc-systems \
-l app.kubernetes.io/part-of=gha-rs-controller \
-o jsonpath="{.items[0].metadata.name}"
)"
if [ -z "${controller_deployment}" ]; then
echo "ERROR: ARC controller deployment was not found."
exit 1
fi
echo "ARC controller deployment: ${controller_deployment}"
kubectl rollout status \
"deployment/${controller_deployment}" \
-n arc-systems \
--timeout=2m
kubectl get deployment,pods \
-n arc-systems \
-o wide
kubectl get crd | grep actions.github.com
available="$(
kubectl get deployment \
"${controller_deployment}" \
-n arc-systems \
-o jsonpath="{.status.availableReplicas}"
)"
echo "Available ARC controller replicas: ${available:-0}"
test "${available:-0}" -eq 2
'14. 🧪 Review and Commit Only the Common Controller Files
git status
git diff --check
git diff --stat
git diff -- \
helm/common/arc-controller/values.yaml \
ansible/roles/arc-controller/defaults/main.yml \
ansible/roles/arc-controller/tasks/main.yml \
ansible/playbooks/shared-k8s/09-install-arc-controller.yml \
.github/workflows/ansible-install-arc-controller.yml
git diff --exit-code -- \
terraform \
ansible/inventories/shared-k8s \
ansible/roles/common \
ansible/roles/containerd \
ansible/roles/kubernetes-common \
ansible/roles/kubernetes-worker \
.github/workflows/ansible-configure-control-planes.yml \
.github/workflows/ansible-configure-dev-workers.yml \
.github/workflows/ansible-configure-qa-workers.yml \
.github/workflows/ansible-configure-prod-workers.ymlThe final git diff --exit-code command must return exit code 0.
git add \
helm/common/arc-controller/values.yaml \
ansible/roles/arc-controller/defaults/main.yml \
ansible/roles/arc-controller/tasks/main.yml \
ansible/playbooks/shared-k8s/09-install-arc-controller.yml \
.github/workflows/ansible-install-arc-controller.yml
git commit -m "Install shared Actions Runner Controller"
git push -u origin feature/install-shared-arc-controller15. ✅ Validate Through dev
gh pr create \
--base dev \
--head feature/install-shared-arc-controller \
--title "Install shared Actions Runner Controller" \
--body "Adds the cluster-shared ARC controller using a pinned Helm chart and an idempotent Ansible workflow. The pull request is for review only; validation starts after the merge creates a dev push."16. 🚀 Promote and Install from prod
gh pr create \
--base prod \
--head dev \
--title "Install shared Actions Runner Controller" \
--body "Promotes the validated cluster-shared ARC controller installation to prod. The pull request itself does not run the workflow; installation starts after merge creates the prod push."Expected production sequence:
Validate values and Ansible target
Verify all 15 Kubernetes nodes are Ready
Install or verify Helm v3.21.0
Render ARC controller chart 0.14.2
Install or reconcile arc-systems/arc
Wait for 2 controller replicas
Verify actions.github.com CRDs17. 🔬 Verify the Shared ARC Controller
ssh \
-i ~/.ssh/id_ed25519_ansible \
-o IdentitiesOnly=yes \
acllc@192.168.8.202 \
'sudo bash -s' <<'REMOTE'
set -euo pipefail
export KUBECONFIG=/etc/kubernetes/admin.conf
HELM=/usr/local/bin/helm
echo "=== HELM VERSION ==="
test -x "${HELM}"
"${HELM}" version --short
echo
echo "=== ARC RELEASE ==="
"${HELM}" list -n arc-systems
"${HELM}" status arc -n arc-systems
echo
echo "=== ARC NAMESPACE ==="
kubectl get namespace arc-systems
echo
echo "=== ARC DEPLOYMENT AND PODS ==="
kubectl get deployment,pods -n arc-systems -o wide
controller_deployment="$(
kubectl get deployment \
-n arc-systems \
-l app.kubernetes.io/part-of=gha-rs-controller \
-o jsonpath='{.items[0].metadata.name}'
)"
test -n "${controller_deployment}"
echo "ARC controller deployment: ${controller_deployment}"
kubectl rollout status \
"deployment/${controller_deployment}" \
-n arc-systems \
--timeout=2m
available="$(
kubectl get deployment \
"${controller_deployment}" \
-n arc-systems \
-o jsonpath='{.status.availableReplicas}'
)"
echo "Available ARC controller replicas: ${available:-0}"
test "${available:-0}" -eq 2
echo
echo "=== ARC CRDS ==="
kubectl get crd | grep actions.github.com
echo
echo "=== ARC CONTROLLER LOGS ==="
kubectl logs \
-n arc-systems \
"deployment/${controller_deployment}" \
--all-containers=true \
--tail=100
echo
echo "=== CLUSTER READINESS ==="
kubectl get nodes -L environment,workload
kubectl get --raw=/readyz
REMOTEShared ARC controller:
Namespace: arc-systems
Helm release: arc
Chart version: 0.14.2
Deployment: arc-gha-rs-controller
Controller pods: 2 available
Placement: Kubernetes control-plane nodes
Leader election: enabled because replicaCount is greater than 1
CRDs: actions.github.com resources present
Not created by this page:
GitHub Apps
organization runner namespaces
Harbor organization credentials
repository runner scale sets
dev, QA, or production listener/runner pods
Next:
onboard the first GitHub organization/product
configure organization and branch isolation
create the first repository runner scale setFROM-SCRATCH ACCEPTANCE CHECKPOINT
Required Helm state
Helm binary: /usr/local/bin/helm
Helm version: v3.21.0
Release: arc
Namespace: arc-systems / Active
Release status: deployed
Controller chart: 0.14.2
Deployment: arc-gha-rs-controller
Desired replicas: 2
Ready replicas: 2
Available replicas: 2
Placement: two Kubernetes control-plane nodes when scheduling permits
Required ARC CRDs
autoscalinglisteners.actions.github.com
autoscalingrunnersets.actions.github.com
ephemeralrunners.actions.github.com
ephemeralrunnersets.actions.github.com
Success rule
Do not continue to organization onboarding until the Helm release is deployed,
two controller replicas are available, all four CRDs exist, and /readyz returns ok.18. 🧩 What Happens on the Next Page
COMMON · NO CHANGE NEEDED
Shared Kubernetes cluster
Helm CLI on cicd-ac-k8s-cp-01
ARC controller namespace and release
ARC controller chart version
ARC controller replicas, resources, logging, and placement
ARC CRDs and controller RBAC
CHANGE PER GITHUB ORG
GitHub App and installation
GitHub App Kubernetes Secret
Harbor project
Harbor runtime and CI robot accounts
organization working folder
CHANGE PER REPOSITORY
GitHub repository URL
runner scale-set name and Helm release
repository runner values file
repository-level GitHub Actions secrets and variables
CHANGE PER DEPLOYMENT BRANCH
dev, qa, or prod runner namespace
environment node selector
environment taint toleration
runner capacity and deployment target credentials19. 🩺 Failure Handling
Helm checksum or download fails
curl -fL -o /tmp/helm.tar.gz \
https://get.helm.sh/helm-v3.21.0-linux-amd64.tar.gz
sha256sum /tmp/helm.tar.gz
echo "Expected: 0093eb572e3d2380f094df162ddb525e219249de88957afe24cfbb19632acd36"dev validation succeeds but Helm and arc-systems are absent
This is expected when only the dev branch has been pushed. The dev workflow validates configuration only. Promote the same commit to prod so the prod push runs the configure job, installs Helm at /usr/local/bin/helm, creates arc-systems, and installs ARC.
test -x /usr/local/bin/helm && /usr/local/bin/helm version --short || echo "Helm is not installed yet"
sudo kubectl --kubeconfig=/etc/kubernetes/admin.conf get namespace arc-systems || echo "ARC namespace is not installed yet"The workflow fails with $2: unbound variable
Do not embed an awk '$2 == "Ready"' expression inside the single-quoted Ansible ad-hoc shell argument. The outer GitHub runner shell can consume $2. Usekubectl wait --for=condition=Ready node --all and explicitly select/bin/bash with ansible_shell_executable, as implemented in the workflow above.
An ARC-only push starts unrelated Ansible workflows
Remove broad ansible/** path filters from component workflows. Keep this ARC workflow limited to the controller values, role, playbook, and workflow file, and use the sharedshared-k8s-ansible concurrency group across cluster-changing Ansible workflows.
Controller pods remain Pending
sudo kubectl --kubeconfig=/etc/kubernetes/admin.conf get pods -n arc-systems -o wide
sudo kubectl --kubeconfig=/etc/kubernetes/admin.conf describe pods -n arc-systems
sudo kubectl --kubeconfig=/etc/kubernetes/admin.conf get nodes --show-labels
sudo kubectl --kubeconfig=/etc/kubernetes/admin.conf describe node cicd-ac-k8s-cp-01 | grep -A4 -E 'Taints:|Labels:'Helm release fails atomically
sudo /usr/local/bin/helm history arc -n arc-systems
sudo /usr/local/bin/helm status arc -n arc-systems || true
sudo kubectl --kubeconfig=/etc/kubernetes/admin.conf get events -n arc-systems --sort-by=.lastTimestamp
sudo kubectl --kubeconfig=/etc/kubernetes/admin.conf get pods -n arc-systems -o wideController is Running but CRDs are missing
sudo kubectl --kubeconfig=/etc/kubernetes/admin.conf get crd | grep actions.github.com || true
sudo /usr/local/bin/helm get manifest arc -n arc-systems | grep -n 'CustomResourceDefinition' || true
sudo /usr/local/bin/helm upgrade --install arc \
oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller \
--namespace arc-systems \
--version 0.14.2 \
--values /etc/ac-cicd-infra/arc-controller/values.yaml \
--atomic --wait --timeout 10mRollback a bad controller upgrade
sudo /usr/local/bin/helm history arc -n arc-systems
sudo /usr/local/bin/helm rollback arc <PREVIOUS_REVISION> \
-n arc-systems \
--wait \
--timeout 10m20. 🏁 Rebuild Status After Successful Completion
FROM-SCRATCH REBUILD CHECKPOINT AFTER THIS PAGE
Expected infrastructure
Load balancer, API VIP, and three control planes operational
Development, QA, and production worker pools Ready
15 Kubernetes nodes Ready
Expected shared ARC controller
Values file and Ansible role committed
Helm v3.21.0 installed on cicd-ac-k8s-cp-01
ARC chart 0.14.2 deployed as arc in arc-systems
Two controller replicas available
Four actions.github.com CRDs installed
Next documentation steps
GitHub organization onboarding
Repository runner scale sets
Deployment-branch runner mappings
Consistency rule
This checkpoint records the required result of executing this page during a clean rebuild;
it does not assume any previously retained VM, Helm release, namespace, or ARC resource.