infra(ci): add Terragrunt-managed CI runner with Tailscale + act_runner

GP1-S instance (8 vCPU, 32GB, 100GB root) provisioned via Terraform
with cloud-init: Docker CE, Tailscale (pre-auth key), Gitea Actions
runner (act_runner v0.3.0). Security group drops all inbound — access
via Tailscale only. Solves: Kapsule pods cannot reach Tailscale IPs.

Also stores Tailscale API key in SCW Secret Manager for future auth
key generation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-02-24 22:15:37 +01:00
parent c8f2dacc5f
commit a47e0cb947
7 changed files with 253 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../../modules/ci-runner"
}
inputs = {
instance_type = "GP1-S" # 8 vCPU, 32GB RAM, 300GB disk
# Sensitive — pass at apply time:
# terragrunt apply -var 'tailscale_auth_key=tskey-auth-...' -var 'runner_token=...'
# Or set TF_VAR_tailscale_auth_key and TF_VAR_runner_token env vars.
}

View File

@@ -0,0 +1,105 @@
#cloud-config
#
# Foxhunt CI Runner (act_runner for Gitea Actions)
# Tailscale-only access, native Docker (no DinD).
#
package_update: true
package_upgrade: true
packages:
- ca-certificates
- curl
- gnupg
write_files:
- path: /opt/act_runner/config.yaml
permissions: '0644'
content: |
log:
level: info
runner:
file: /opt/act_runner/.runner
capacity: ${runner_capacity}
timeout: 3h
insecure: false
fetch_timeout: 5s
fetch_interval: 2s
cache:
enabled: true
dir: /opt/act_runner/cache
container:
network: host
privileged: false
options:
valid_volumes: []
- path: /etc/systemd/system/act-runner.service
permissions: '0644'
content: |
[Unit]
Description=Gitea Actions Runner
After=docker.service tailscaled.service
Requires=docker.service
[Service]
Type=simple
WorkingDirectory=/opt/act_runner
ExecStart=/usr/local/bin/act_runner daemon --config /opt/act_runner/config.yaml
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
runcmd:
# --- Docker CE ---
- install -m 0755 -d /etc/apt/keyrings
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg
- chmod a+r /etc/apt/keyrings/docker.gpg
- |
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" > /etc/apt/sources.list.d/docker.list
- apt-get update -qq
- apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin
- systemctl enable docker
- systemctl start docker
# --- Tailscale ---
- curl -fsSL https://tailscale.com/install.sh | sh
- tailscale up --authkey=${tailscale_auth_key} --hostname=${instance_name} --ssh
# Wait for Tailscale connectivity
- |
for i in $(seq 1 30); do
TS_IP=$(tailscale ip -4 2>/dev/null)
if [ -n "$TS_IP" ]; then
echo "Tailscale IP: $TS_IP"
break
fi
sleep 2
done
# --- act_runner ---
- |
ACT_RUNNER_VERSION="0.3.0"
curl -fsSL "https://dl.gitea.com/act_runner/$${ACT_RUNNER_VERSION}/act_runner-$${ACT_RUNNER_VERSION}-linux-amd64" \
-o /usr/local/bin/act_runner
chmod +x /usr/local/bin/act_runner
# Register runner with Gitea (via Tailscale)
- |
cd /opt/act_runner
act_runner register --no-interactive \
--instance "${gitea_url}" \
--token "${runner_token}" \
--name "${instance_name}" \
--labels "${runner_labels}"
# Enable and start runner service
- systemctl daemon-reload
- systemctl enable act-runner
- systemctl start act-runner
- echo "PROVISIONING_COMPLETE" > /var/log/cloud-init-done

View File

@@ -0,0 +1,38 @@
# Security group: drop all inbound (Tailscale-only access)
resource "scaleway_instance_security_group" "ci_runner" {
name = "${var.instance_name}-sg"
zone = var.zone
inbound_default_policy = "drop"
outbound_default_policy = "accept"
}
resource "scaleway_instance_ip" "ci_runner" {
zone = var.zone
}
resource "scaleway_instance_server" "ci_runner" {
name = var.instance_name
type = var.instance_type
image = var.image
zone = var.zone
ip_id = scaleway_instance_ip.ci_runner.id
security_group_id = scaleway_instance_security_group.ci_runner.id
root_volume {
size_in_gb = var.root_volume_size_gb
}
user_data = {
cloud-init = templatefile("${path.module}/cloud-init.yaml.tpl", {
tailscale_auth_key = var.tailscale_auth_key
instance_name = var.instance_name
gitea_url = var.gitea_url
runner_token = var.runner_token
runner_labels = var.runner_labels
runner_capacity = var.runner_capacity
})
}
tags = ["ci", "foxhunt"]
}

View File

@@ -0,0 +1,14 @@
output "instance_id" {
description = "ID of the CI runner instance"
value = scaleway_instance_server.ci_runner.id
}
output "public_ip" {
description = "Public IP (needed for Tailscale outbound tunnel)"
value = scaleway_instance_ip.ci_runner.address
}
output "tailscale_hostname" {
description = "Tailscale hostname of the CI runner"
value = var.instance_name
}

View File

@@ -0,0 +1,63 @@
variable "zone" {
description = "Scaleway zone for the CI runner instance"
type = string
}
variable "region" {
description = "Scaleway region"
type = string
}
variable "instance_type" {
description = "Instance type for the CI runner"
type = string
default = "GP1-S"
}
variable "instance_name" {
description = "Name of the CI runner instance"
type = string
default = "vm-fxhnt-ci"
}
variable "image" {
description = "OS image for the instance"
type = string
default = "ubuntu_noble"
}
variable "root_volume_size_gb" {
description = "Root volume size in GB (Docker images + build cache)"
type = number
default = 100
}
variable "tailscale_auth_key" {
description = "Tailscale pre-auth key for joining the tailnet"
type = string
sensitive = true
}
variable "gitea_url" {
description = "Gitea instance URL"
type = string
default = "https://git.fxhnt.ai"
}
variable "runner_token" {
description = "Gitea Actions runner registration token"
type = string
sensitive = true
}
variable "runner_labels" {
description = "Runner labels for act_runner"
type = string
default = "ubuntu-latest:docker://catthehacker/ubuntu:act-latest"
}
variable "runner_capacity" {
description = "Number of concurrent jobs"
type = number
default = 1
}

View File

@@ -31,3 +31,15 @@ resource "scaleway_secret_version" "db_password" {
data = base64encode(random_password.db_password.result)
region = var.region
}
resource "scaleway_secret" "tailscale_api_key" {
name = "foxhunt-tailscale-api-key"
project_id = var.project_id
region = var.region
}
resource "scaleway_secret_version" "tailscale_api_key" {
secret_id = scaleway_secret.tailscale_api_key.id
data = base64encode(var.tailscale_api_key)
region = var.region
}

View File

@@ -7,3 +7,9 @@ variable "project_id" {
description = "Scaleway project ID"
type = string
}
variable "tailscale_api_key" {
description = "Tailscale API key for generating pre-auth keys"
type = string
sensitive = true
}