ci: gitea: runners infra
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# OpenTofu working directory and downloaded modules/providers.
|
||||
.terraform/
|
||||
.terraform.lock.hcl
|
||||
|
||||
# State must live in the S3 backend for production. Local state is allowed only
|
||||
# for throwaway syntax checks with `tofu init -backend=false` and must not be
|
||||
# committed.
|
||||
terraform.tfstate
|
||||
terraform.tfstate.*
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
crash.log
|
||||
crash.*.log
|
||||
|
||||
# Plans can contain secrets or derived infrastructure data.
|
||||
*.tfplan
|
||||
*.plan
|
||||
kubeconfig
|
||||
kubeconfig.yaml
|
||||
*_kubeconfig.yaml
|
||||
|
||||
# Variable files commonly carry credentials. Keep production inputs in SOPS or
|
||||
# external environment/configuration, not in checked-in files.
|
||||
*.tfvars
|
||||
*.tfvars.json
|
||||
override.tf
|
||||
override.tf.json
|
||||
*_override.tf
|
||||
*_override.tf.json
|
||||
@@ -0,0 +1,90 @@
|
||||
# Gitea runner OpenTofu backend contract
|
||||
|
||||
This directory defines the safe backend, provider contract, and kube-hetzner
|
||||
cluster stack for the Gitea runner Kubernetes cluster.
|
||||
|
||||
## Required backend
|
||||
|
||||
Production state must use the OpenTofu S3 backend in `backend.tf`:
|
||||
|
||||
- bucket: `gitea-runner-hectic-lab`
|
||||
- key: `gitea-runners/kube-hetzner/terraform.tfstate`
|
||||
- region: `fsn1`, aligned with the target Hetzner location
|
||||
- encryption: `encrypt = true`
|
||||
- locking: `use_lockfile = true` where the selected S3-compatible endpoint
|
||||
supports it
|
||||
|
||||
Before any production `tofu init`, verify the S3-compatible endpoint, credential
|
||||
source, bucket versioning, encryption behavior, and lockfile support for the
|
||||
chosen object-storage provider. Keep backend authentication externalized through
|
||||
environment variables, AWS-compatible shared config, or the production secret
|
||||
injection path from Task 3. Do not add `access_key`, `secret_key`, Hetzner
|
||||
tokens, runner tokens, kubeconfig material, or decrypted SOPS data to checked-in
|
||||
OpenTofu files.
|
||||
|
||||
## Local state safety
|
||||
|
||||
Production local state is forbidden. Only syntax-only validation/prototyping may
|
||||
use local state, and it must use backend-disabled initialization:
|
||||
|
||||
```sh
|
||||
tofu -chdir=infra/gitea-runners/opentofu init -backend=false
|
||||
tofu -chdir=infra/gitea-runners/opentofu validate
|
||||
```
|
||||
|
||||
Fail the run if production local state appears:
|
||||
|
||||
```sh
|
||||
test ! -e infra/gitea-runners/opentofu/terraform.tfstate
|
||||
test ! -e infra/gitea-runners/opentofu/terraform.tfstate.backup
|
||||
grep -R 'backend "s3"' infra/gitea-runners/opentofu
|
||||
```
|
||||
|
||||
The `.gitignore` in this directory blocks local state, plans, downloaded
|
||||
providers/modules, and variable files from being committed. Treat any local
|
||||
state file as disposable validation residue, never as production state.
|
||||
|
||||
## Provider and module pins
|
||||
|
||||
`versions.tf` pins the OpenTofu-compatible Hetzner Cloud provider to
|
||||
`hetznercloud/hcloud` version `1.60.1`. kube-hetzner research for this plan
|
||||
observed module version `2.19.3`, source `kube-hetzner/kube-hetzner/hcloud`, and
|
||||
module minimum hcloud provider requirement `>= 1.59.0`; these values are recorded
|
||||
as locals so Task 5 can wire the module without re-opening the version contract.
|
||||
|
||||
`providers.tf` leaves the `hcloud` provider empty so authentication comes from
|
||||
the provider's external environment/config mechanisms such as `HCLOUD_TOKEN`.
|
||||
Do not set token values in `.tf` or `.tfvars` files.
|
||||
|
||||
## Cluster shape
|
||||
|
||||
The default cluster is deliberately fixed-size:
|
||||
|
||||
- cluster name: `gitea-runners`
|
||||
- Hetzner location: `fsn1`
|
||||
- private network region: `eu-central`
|
||||
- control plane: one `cpx21` node in pool `control-plane`
|
||||
- workers: three `cpx31` nodes in pool `runner-workers`
|
||||
- storage: Hetzner CSI enabled with expected StorageClass `hcloud-volumes`
|
||||
- Longhorn: disabled
|
||||
- autoscaling/KEDA: not enabled in this stack
|
||||
|
||||
The three default workers are sized for the initial five trusted privileged DinD
|
||||
jobs. To scale toward ten jobs later, keep autoscaling disabled and either raise
|
||||
`worker_count` to `5` or increase `worker_server_type`, then run a fresh
|
||||
`tofu plan` and the Task 11 Kubernetes pressure checks before applying.
|
||||
|
||||
Required inputs must come from environment or secret injection, for example
|
||||
`TF_VAR_hcloud_token`, `TF_VAR_ssh_public_key`, and `TF_VAR_ssh_private_key`.
|
||||
Do not commit `.tfvars` files. kube-hetzner v2.19.3 writes the generated
|
||||
kubeconfig to `./<cluster_name>_kubeconfig.yaml` when `create_kubeconfig` is
|
||||
enabled; this path is ignored as operational secret material.
|
||||
|
||||
## Known state caveat
|
||||
|
||||
kube-hetzner may thread `hcloud_token` into Kubernetes secrets/state through its
|
||||
internal `kube_system_secrets` handling. This task does not claim that risk is
|
||||
solved. Task 5 must verify the generated plan and state before production apply
|
||||
and prove that Hetzner tokens, S3 credentials, runner tokens, kubeconfig private
|
||||
keys, and decrypted secrets are absent from committed files and unsafe state
|
||||
evidence.
|
||||
@@ -0,0 +1,16 @@
|
||||
terraform {
|
||||
backend "s3" {
|
||||
bucket = "gitea-runner-hectic-lab"
|
||||
key = "gitea-runners/kube-hetzner/terraform.tfstate"
|
||||
region = "fsn1"
|
||||
encrypt = true
|
||||
use_lockfile = true
|
||||
}
|
||||
}
|
||||
|
||||
check "remote_state_contract" {
|
||||
assert {
|
||||
condition = local.production_remote_state
|
||||
error_message = "Production OpenTofu state must use the configured S3 backend; local production state is forbidden."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
locals {
|
||||
default_storage_class = "hcloud-volumes"
|
||||
|
||||
control_plane_nodepools = [
|
||||
{
|
||||
name = "control-plane"
|
||||
server_type = var.control_plane_server_type
|
||||
location = var.hetzner_location
|
||||
labels = []
|
||||
taints = []
|
||||
count = 1
|
||||
},
|
||||
]
|
||||
|
||||
agent_nodepools = [
|
||||
{
|
||||
name = "runner-workers"
|
||||
server_type = var.worker_server_type
|
||||
location = var.hetzner_location
|
||||
labels = ["node-role.hectic-lab/gitea-runner=true"]
|
||||
taints = []
|
||||
count = var.worker_count
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
module "kube_hetzner" {
|
||||
source = "kube-hetzner/kube-hetzner/hcloud"
|
||||
version = "2.19.3"
|
||||
|
||||
providers = {
|
||||
hcloud = hcloud
|
||||
}
|
||||
|
||||
hcloud_token = var.hcloud_token
|
||||
ssh_public_key = var.ssh_public_key
|
||||
ssh_private_key = var.ssh_private_key
|
||||
|
||||
cluster_name = var.cluster_name
|
||||
base_domain = var.base_domain
|
||||
|
||||
# kube-hetzner v2.19.3 writes <cluster_name>_kubeconfig.yaml; outputs below
|
||||
# expose that expected path without outputting kubeconfig private key material.
|
||||
create_kubeconfig = true
|
||||
|
||||
network_region = var.network_region
|
||||
load_balancer_location = var.hetzner_location
|
||||
control_plane_nodepools = local.control_plane_nodepools
|
||||
agent_nodepools = local.agent_nodepools
|
||||
|
||||
# Hetzner CSI is the required StorageClass provider for runner PVCs.
|
||||
disable_hetzner_csi = false
|
||||
|
||||
# Longhorn is intentionally off; the initial runner PVCs use Hetzner CSI only.
|
||||
enable_longhorn = false
|
||||
|
||||
# Scaling note: for 10 trusted DinD jobs later, keep autoscaling disabled and
|
||||
# raise worker_count to 5 or increase worker_server_type after validating pod
|
||||
# CPU, memory, and ephemeral-storage pressure in Task 11.
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
output "kubeconfig_path" {
|
||||
description = "Path where kube-hetzner writes kubeconfig after apply. The file is operational secret material and must not be committed."
|
||||
value = coalesce(var.kubeconfig_path, "./${var.cluster_name}_kubeconfig.yaml")
|
||||
}
|
||||
|
||||
output "cluster_name" {
|
||||
description = "kube-hetzner cluster name."
|
||||
value = var.cluster_name
|
||||
}
|
||||
|
||||
output "node_pool_names" {
|
||||
description = "Control-plane and worker node pool names used by this stack."
|
||||
value = {
|
||||
control_plane = [for pool in local.control_plane_nodepools : pool.name]
|
||||
workers = [for pool in local.agent_nodepools : pool.name]
|
||||
}
|
||||
}
|
||||
|
||||
output "default_storage_class" {
|
||||
description = "Default Hetzner CSI StorageClass expected for runner PVCs."
|
||||
value = local.default_storage_class
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
provider "hcloud" {}
|
||||
@@ -0,0 +1,74 @@
|
||||
variable "hcloud_token" {
|
||||
description = "Hetzner Cloud API token for kube-hetzner. Set with TF_VAR_hcloud_token or secret injection only; never commit it. kube-hetzner may place this value into Kubernetes secret resources/state, so scan plans before apply."
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "ssh_public_key" {
|
||||
description = "SSH public key installed on cluster nodes. Supply from an external file or secret injection path."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "ssh_private_key" {
|
||||
description = "SSH private key used by kube-hetzner during bootstrap. Supply from an external file or secret injection path; never commit it."
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "cluster_name" {
|
||||
description = "Name for the kube-hetzner runner cluster."
|
||||
type = string
|
||||
default = "gitea-runners"
|
||||
|
||||
validation {
|
||||
condition = can(regex("^[a-z0-9-]+$", var.cluster_name))
|
||||
error_message = "cluster_name must contain only lowercase letters, numbers, and dashes."
|
||||
}
|
||||
}
|
||||
|
||||
variable "hetzner_location" {
|
||||
description = "Hetzner Cloud location for all node pools. fsn1 keeps the first runner cluster in Falkenstein."
|
||||
type = string
|
||||
default = "fsn1"
|
||||
}
|
||||
|
||||
variable "network_region" {
|
||||
description = "Hetzner private network region. eu-central covers fsn1."
|
||||
type = string
|
||||
default = "eu-central"
|
||||
}
|
||||
|
||||
variable "control_plane_server_type" {
|
||||
description = "Default control-plane server type. cpx21 is small but leaves headroom for kube-system workloads."
|
||||
type = string
|
||||
default = "cpx21"
|
||||
}
|
||||
|
||||
variable "worker_server_type" {
|
||||
description = "Default worker server type for the initial trusted DinD runner pool. Three cpx31 workers provide enough headroom for five privileged jobs before Task 11 scaling validation."
|
||||
type = string
|
||||
default = "cpx31"
|
||||
}
|
||||
|
||||
variable "worker_count" {
|
||||
description = "Fixed worker count. Increase to 5 or choose a larger worker_server_type later to target 10 concurrent DinD jobs; do not enable autoscaling in this stack."
|
||||
type = number
|
||||
default = 3
|
||||
|
||||
validation {
|
||||
condition = var.worker_count >= 1
|
||||
error_message = "worker_count must be at least 1."
|
||||
}
|
||||
}
|
||||
|
||||
variable "kubeconfig_path" {
|
||||
description = "Expected kubeconfig path. kube-hetzner v2.19.3 writes this as <cluster_name>_kubeconfig.yaml when create_kubeconfig is true."
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "base_domain" {
|
||||
description = "Optional base domain for node reverse DNS. Empty keeps kube-hetzner defaults."
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
terraform {
|
||||
required_version = ">= 1.10.1"
|
||||
|
||||
required_providers {
|
||||
hcloud = {
|
||||
source = "hetznercloud/hcloud"
|
||||
version = "1.60.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
locals {
|
||||
kube_hetzner_module_source = "kube-hetzner/kube-hetzner/hcloud"
|
||||
kube_hetzner_module_version = "2.19.3"
|
||||
hcloud_provider_minimum = ">= 1.59.0"
|
||||
production_remote_state = true
|
||||
}
|
||||
Reference in New Issue
Block a user