Files
a13labs.infra/.opencode/plans/ci-orchestrator.md
T

234 lines
9.4 KiB
Markdown
Raw Normal View History

# 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/<ci-namespace>/act-runner:<tag>`
### 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 = "<user>"
token = "<token>"
}
resource "gitea_repository_webhook" "ci_orchestrator" {
username = "<org-or-user>"
name = "<repo-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.