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:
15
infra/live/production/ci-runner/terragrunt.hcl
Normal file
15
infra/live/production/ci-runner/terragrunt.hcl
Normal 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.
|
||||
}
|
||||
105
infra/modules/ci-runner/cloud-init.yaml.tpl
Normal file
105
infra/modules/ci-runner/cloud-init.yaml.tpl
Normal 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
|
||||
38
infra/modules/ci-runner/main.tf
Normal file
38
infra/modules/ci-runner/main.tf
Normal 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"]
|
||||
}
|
||||
14
infra/modules/ci-runner/outputs.tf
Normal file
14
infra/modules/ci-runner/outputs.tf
Normal 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
|
||||
}
|
||||
63
infra/modules/ci-runner/variables.tf
Normal file
63
infra/modules/ci-runner/variables.tf
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user