Deploy the GitHub Actions self-hosted runner VM. This VM hosts the container registry, ISO file server, and GitHub Actions runners for automated workflows.
Note: This is a one-time bootstrap procedure that must be run from a local workstation with vCenter access.
- 00-prerequisites.md completed
- Workstation with vCenter network access
- Terraform >= 1.13.0 installed locally
- Ansible >= 2.15 installed locally
- SSH key pair generated
Phase 0 is a chicken-egg problem: we need a self-hosted runner to deploy infrastructure, but the runner doesn't exist yet.
Solution: Bootstrap Phase 0 locally from a workstation with vCenter access.
┌─────────────────────────────────────────────────────────────────┐
│ PHASE 0: LOCAL BOOTSTRAP (from workstation with vCenter access)│
│ ───────────────────────────────────────────────────────────── │
│ terraform apply + ansible-playbook │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ PHASES 0b-3: AUTOMATED (self-hosted runner + container) │
│ ───────────────────────────────────────────────────────────── │
│ runs-on: self-hosted │
│ container: ghcr.io/mahowlin/github-actions-container │
└─────────────────────────────────────────────────────────────────┘
# Clone your organization's runner VM repository
git clone https://github.com/<YOUR_ORG>/<runner-vm-repo>.git
cd <runner-vm-repo>cd tf
cp ../environments/_template/terraform.tfvars.example terraform.tfvarsEdit terraform.tfvars with your environment values:
# vCenter Connection
vsphere_server = "vcenter.example.com"
vsphere_user = "administrator@example.com"
vsphere_password = "<VCENTER_PASSWORD>"
# VM Location
vsphere_datacenter = "MYLAB-DC"
vsphere_cluster = "infra-cluster"
vsphere_datastore = "infra-datastore"
vsphere_network = "VM Network"
vsphere_template = "ubuntu-24.04-template"
vsphere_folder = "AI-Pod"
# VM Configuration
vm_name = "saif-github-runner"
vm_hostname = "saif-github-runner"
vm_domain = "example.com"
vm_cpu = 8
vm_memory = 32768
vm_disk_os_size = 100
vm_disk_data_size = 500
# Network Configuration
vm_ipv4_address = "10.0.0.10"
vm_ipv4_netmask = 23
vm_ipv4_gateway = "10.0.0.1"
vm_dns_servers = ["10.0.0.53", "10.0.0.54"]terraform init
terraform plan -out=tfplanExpected Output:
Plan: 1 to add, 0 to change, 0 to destroy.
terraform apply tfplanExpected Output:
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
runner_vm_ip = "10.0.0.10"
Verification:
ping -c 3 10.0.0.10
ssh ubuntu@10.0.0.10 "hostname -f"
# Expected: runner.example.comcd ../ansible
cp ../environments/_template/hosts.ini.example hosts.iniEdit hosts.ini:
[runner]
10.0.0.10 ansible_user=ubuntu
[runner:vars]
ansible_ssh_private_key_file=~/.ssh/id_rsaGet a GitHub runner registration token:
- Navigate to: https://github.com/mahowlin/saif-ai-pod/settings/actions/runners/new
- Copy the registration token (valid for 1 hour)
Run the Ansible playbook:
RUNNER_TOKEN="<REGISTRATION_TOKEN>"
ansible-playbook -i hosts.ini playbooks/configure-runner.yaml \
--extra-vars "runner_token=${RUNNER_TOKEN}"Expected Output:
PLAY RECAP ***
10.0.0.10 : ok=28 changed=25 unreachable=0 failed=0
SSH to runner VM and verify services:
ssh ubuntu@10.0.0.10
# Test container registry
curl -s http://localhost:5000/v2/_catalog
# Expected: {"repositories":[]}
# Test web server (ISO hosting)
curl -s -I http://localhost:8080/
# Expected: HTTP/1.1 200 OK
# Test Terraform state backend
curl -s http://localhost:8081/health
# Expected: OKNavigate to: https://github.com/mahowlin/saif-ai-pod/settings/actions/runners
Expected: saif-runner-1, saif-runner-2, saif-runner-3 show as "Idle"
After deployment, the runner VM provides:
| Service | Container | Port | Storage | Purpose |
|---|---|---|---|---|
| Container Registry | registry:2 |
5000 | /data/registry |
Air-gap image mirror |
| Web Server (ISOs) | nginx |
8080 | /data/images |
Agent ISO hosting |
| Terraform State | nimbolus/terraform-backend |
8081 | /data/tfstate |
State persistence |
| GitHub Runners | myoung34/github-runner |
- | /data/runner-* |
CI/CD execution |
Checklist:
- VM deployed and accessible via SSH
- Container registry responds at
:5000 - Web server responds at
:8080 - Terraform backend responds at
:8081 - GitHub runners show as "Idle" in repository settings
- Check VM power state in vCenter
- Verify network connectivity to VLAN 130
- Check firewall rules allow SSH
sudo docker logs registry
sudo docker restart registry- Verify token is valid (expires after 1 hour)
- Check runner logs:
sudo docker logs runner-1 - Regenerate token and re-run Ansible
If runners are in restart loop:
# Stop and remove broken containers
ssh ubuntu@10.0.0.10 'sudo docker stop runner-1 runner-2 runner-3 && sudo docker rm runner-1 runner-2 runner-3'
# Get fresh token and re-run Ansible
cd <runner-vm-repo>
RUNNER_TOKEN=$(gh api -X POST /orgs/<YOUR_ORG>/actions/runners/registration-token --jq '.token')
ansible-playbook -i environments/example/hosts.ini \
ansible/playbooks/configure-runner.yaml \
--extra-vars "runner_token=${RUNNER_TOKEN}" \
--tags runnerTo destroy the runner VM:
cd <runner-vm-repo>/tf
terraform destroy -auto-approveWarning: This removes all services including the container registry and mirrored images.
Continue to 02-image-mirroring.md to populate the container registry.