# Gitea → MicroK8s Webhook Orchestrator → Scaleway Containers (CI Runner System) A lightweight CI orchestration system where a self-hosted Gitea instance triggers a Go-based webhook service running inside MicroK8s. The service decides whether to run jobs locally or launch ephemeral CI runners in Scaleway Serverless Containers. --- ## 1. High-Level Architecture ```text On-Premises (MicroK8s Cluster) ┌──────────────────────────────────────────────────────────────┐ │ Gitea │ │ │ │ Repo push / PR event │ └───────────────┬──────────────────────────────────────────────┘ │ Webhook (internal cluster network) ▼ ┌──────────────────────────────────────────────────────────────┐ │ ci-orchestrator (Go service) │ │ │ │ - Validates webhook signature (HMAC) │ │ - Parses event payload │ │ - Applies routing rules │ │ - Decides runner target (local vs cloud) │ └───────────────┬──────────────────────────────────────────────┘ │ HTTPS API call (authenticated) ▼ ──────────────────────── Internet ─────────────────────────────── │ ▼ ┌──────────────────────────────────────────────────────────────┐ │ Scaleway Serverless Containers API │ │ │ │ - Starts ephemeral container │ │ - Runs Gitea Runner │ │ - Executes CI job │ │ - Shuts down after completion │ └──────────────────────────────────────────────────────────────┘ ``` --- ## 2. Current State | Component | Location | Status | |---|---|---| | **Gitea** | `prod-01.alexpires.me` (K8s) | Deployed via Terraform module, TLS, ingress, S3 storage, MariaDB | | **MicroK8s** | `prod-01`, `dev-01` | nginx ingress, cert-manager, multiple apps running | | **Scaleway** | `fr-par` | Registry + S3 buckets + IAM keys exist, no Serverless Containers yet | | **Gitea Runners** | `ci-01.lab.alexpires.me` | Windows Podman + native runners already deployed | | **DNS** | CoreDNS in `dev-01` | `ci-01 → 10.19.4.207` already resolved | --- ## 3. Design Decisions 1. **Orchestrator cluster:** `prod-01` for low latency to Gitea 2. **Routing:** Cloud runners only trigger when `cloud` label is present 3. **Runner image:** `gitea/act_runner` (official) 4. **Webhook registration:** Terraform-managed via `go-gitea/gitea` provider --- ## 4. Implementation Plan ### Phase 1: Scaleway IaC (`terraform/iac/scaleway/`) **New Terraform resources:** - `scaleway_container_namespace` — CI runners namespace - `scaleway_container` — the `gitea/act_runner` container (ephemeral, privacy=private) - `scaleway_iam_policy` — `ContainersInvoke` permission for the CI orchestrator app - `scaleway_container_domain` (optional) — custom domain if desired **IAM:** - New app `ci_runner_app` with policy granting `ContainersInvoke` - API key for the Go service to invoke containers **Existing reuse:** - `data.terraform_remote_state.scaleway` in `prod-01` for outputs - Existing `scaleway_registry_namespace` for pushing the runner image ### Phase 2: Go Service (`ci-orchestrator/`) ``` ci-orchestrator/ ├── go.mod ├── main.go # Entry: HTTP server ├── Dockerfile # Multi-stage, distroless ├── .dockerignore └── internal/ ├── webhook/ │ ├── gitea.go # Parse Gitea push/PR payloads │ └── hmac.go # HMAC-SHA256 validation ├── router/ │ ├── router.go # Check if job has "cloud" label │ └── rules.go # Config-based routing rules └── runner/ └── scaleway.go # Invoke SCW Serverless Container ``` **Routing logic:** - Parse webhook → extract repo, branch, commit, labels (from PR body or commit message convention) - If `cloud` label → invoke Scaleway container - If no label → ignore (existing runners on `ci-01` handle it) **Endpoints:** - `POST /webhook/gitea` — main webhook receiver - `GET /health` — health check ### Phase 3: Runner Container Image ```dockerfile FROM gitea/act_runner:latest ENV GITEA_RUNNER_IGNORE_RUNNING_JOBS_IN_SCRIPT=true # Entrypoint configured via Terraform env vars ``` Push to: `rg.fr-par.scw.cloud//act-runner:` ### Phase 4: Terraform K8s Deployment (`terraform/modules/apps/ci-orchestrator/`) Deployed in `prod-01` namespace: | Resource | Purpose | |---|---| | `kubernetes_namespace` | `ci-orchestrator` | | `kubernetes_deployment` | Go service (hardened security context like other modules) | | `kubernetes_service` | ClusterIP, port 8080 | | `kubernetes_ingress_v1` | Ingress via nginx, path `/webhook/gitea` | | `kubernetes_config_map` | Routing rules config | | `kubernetes_secret` | HMAC key, Scaleway API credentials, Gitea URL | ### Phase 5: Gitea Webhook Registration ```hcl provider "gitea" { address = "https://code.alexpires.me" username = "" token = "" } resource "gitea_repository_webhook" "ci_orchestrator" { username = "" name = "" type = "gitea" url = "https://code.alexpires.me/webhook/gitea" secret = var.webhook_secret content_type = "json" branch_filter = "*" events = ["push", "pull_request"] active = true } ``` Can be per-repo or a single wildcard. The Go service routes based on labels. ### Phase 6: CI Flow ``` push/PR to Gitea │ ▼ Webhook → ci-orchestrator (prod-01) │ ├─ no cloud label → ignore (local runners handle it) │ └─ cloud label → invoke Scaleway container │ ▼ gitea/act_runner starts Registers with Gitea Receives and executes job Exits → container shuts down (cost: per-second) ``` --- ## 5. Provider Compatibility ### `go-gitea/gitea` (v0.7.0) - `gitea_repository_webhook` resource — creates and manages webhooks - Supports: `url`, `secret`, `content_type`, `events`, `branch_filter`, `active`, `type` ### `scaleway/scaleway` (v2.77.1) - `scaleway_container` — Serverless Container resource - `privacy` can be set to `private` (requires IAM auth) - `min_scale = 0`, `max_scale = 1` for ephemeral execution - Supports `command`, `args`, `environment_variables`, `secret_environment_variables` - `scaleway_container_namespace` — namespace for containers - `scaleway_iam_policy` — IAM policies for container invocation --- ## 7. Knowns (Updated) ### Gitea - URL: `https://code.alexpires.me` - Admin user: `alexandre.pires` - API token: `${GITEA_ADMIN_TOKEN}` (via sectool) - Provider config: `gitea_credentials` in `terraform/iac/scaleway/` or `prod-01/` ### Webhook registration - Target: all existing repos + new ones - Method: one-time Ansible script to register webhooks on all repos, then run once per new repo - Webhook target URL: `https://code.alexpires.me/webhook/gitea` (points to Go service ingress) ### Runner resources - CPU: 2 vCPU - Memory: 4Gi - Image: `gitea/act_runner:latest` (or pinned version) ### Repository - Moved to Gitea: `https://code.alexpires.me/alexpires.me/a13labs.infra` - Origin remote updated to `ssh://git@code.alexpires.me:22/alexpires.me/a13labs.infra.git` - All branches and tags pushed ### CI repo workflow - `ci-01` runner builds Terraform images using Gitea Actions - Images pushed to Scaleway Registry - Terraform state managed via S3 on Scaleway --- ## 8. Open Decisions (to be addressed during implementation) 1. **Routing labels** — How will the Go service detect "cloud" intent? Options: - PR body labels (`[cloud]` in title/body) - Commit message convention (`[cloud]` prefix) - Branch naming convention - Separate "cloud" branch filter per repo 2. **Go service ingress path** — Current plan: `/webhook/gitea`. Need to ensure it doesn't conflict with Gitea's own routes. 3. **Scaling strategy** — `max_scale=1` per invocation, but multiple concurrent jobs? May need pool or queue.