Files
JMR-devandClaude Opus 5.5 ec4de0c6c4 Run Gitea Actions jobs on a dedicated GKE cluster
gitea:runnersEnabled creates a zonal GKE Standard cluster, used for
nothing else, running the official gitea/runner image in rootless
Docker-in-Docker, so workflows can `docker build`. Off by default; with
the gate off the stack only gains the container API.

- private spot nodes, egress through Cloud NAT on a static IP; control
  plane reachable only through its IAM-gated DNS endpoint
- the registration token is a Secret Manager secret created by
  scripts/bootstrap.sh; Gitea reads it from a file, the runner pod
  mounts it through the Secret Manager CSI add-on with Workload
  Identity. Pulumi never sees the value
- a NetworkPolicy keeps job containers off the metadata server and
  private ranges, so jobs hold no GCP identity
- Caddy routes /api/actions, /api/actions_pipeline and the artifact
  service around the WAF (protobuf and PUT/PATCH uploads); fail2ban
  ignores the NAT address
- docs/runners.md: threat model, cost, enable/disable, token rotation

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:31:15 +07:00

222 lines
7.9 KiB
Go

// Package compute builds the data disk, its snapshot schedule, and the instance.
package compute
import (
"fmt"
"os"
"strconv"
gcpcompute "github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/compute"
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/serviceaccount"
"github.com/pulumi/pulumi-gcp/sdk/v9/go/gcp/storage"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"gitea-infra/pkg/config"
"gitea-infra/pkg/network"
"gitea-infra/pkg/registry"
)
// DataDiskDeviceName is what the disk shows up as under /dev/disk/by-id/google-*
// inside the guest. vm/bootstrap.sh mounts it by UUID, but it needs this name to
// find the device the first time, before there is a filesystem to have a UUID.
const DataDiskDeviceName = "gitea-data"
type Instance struct {
VM *gcpcompute.Instance
Disk *gcpcompute.Disk
}
func New(
ctx *pulumi.Context,
cfg *config.Config,
net *network.Network,
sa *serviceaccount.Account,
buckets *storage.Bucket,
backupBucket *storage.Bucket,
configHash string,
bootstrapPath string,
deps []pulumi.Resource,
) (*Instance, error) {
opts := pulumi.DependsOn(deps)
// A separate disk from the boot disk, so replacing the instance -- for a
// machine-type change, an image refresh, anything -- never touches the
// repositories. Protect(true) makes an accidental delete a two-step mistake.
//
// This is the disk that holds everything you would miss: repositories, the
// SQLite database, and LFS objects. gitea:dataDiskGb sizes it. The boot disk
// is separate and sized by gitea:bootDiskGb; growing that one buys nothing.
// A pd-balanced disk can be grown in place later, never shrunk.
disk, err := gcpcompute.NewDisk(ctx, "gitea-data", &gcpcompute.DiskArgs{
Name: pulumi.String("gitea-data"),
Zone: pulumi.String(cfg.Zone),
Size: pulumi.Int(cfg.DataDiskGB),
Type: pulumi.String("pd-balanced"),
Description: pulumi.String("Gitea repositories, SQLite database, and LFS objects"),
}, opts, pulumi.Protect(true))
if err != nil {
return nil, err
}
// The safety net for the weekly unattended-reboot window, and for everything
// else that goes wrong at 3am.
policy, err := gcpcompute.NewResourcePolicy(ctx, "gitea-snapshots", &gcpcompute.ResourcePolicyArgs{
Name: pulumi.String("gitea-daily-snapshots"),
Region: pulumi.String(cfg.Region),
SnapshotSchedulePolicy: &gcpcompute.ResourcePolicySnapshotSchedulePolicyArgs{
Schedule: &gcpcompute.ResourcePolicySnapshotSchedulePolicyScheduleArgs{
DailySchedule: &gcpcompute.ResourcePolicySnapshotSchedulePolicyScheduleDailyScheduleArgs{
DaysInCycle: pulumi.Int(1),
StartTime: pulumi.String("03:00"),
},
},
RetentionPolicy: &gcpcompute.ResourcePolicySnapshotSchedulePolicyRetentionPolicyArgs{
MaxRetentionDays: pulumi.Int(14),
// Keep the history even if the disk itself is deleted.
OnSourceDiskDelete: pulumi.String("KEEP_AUTO_SNAPSHOTS"),
},
SnapshotProperties: &gcpcompute.ResourcePolicySnapshotSchedulePolicySnapshotPropertiesArgs{
StorageLocations: pulumi.String(cfg.Region),
Labels: pulumi.StringMap{"app": pulumi.String("gitea")},
},
},
}, opts)
if err != nil {
return nil, err
}
if _, err := gcpcompute.NewDiskResourcePolicyAttachment(ctx, "gitea-data-snapshots", &gcpcompute.DiskResourcePolicyAttachmentArgs{
Name: policy.Name,
Disk: disk.Name,
Zone: pulumi.String(cfg.Zone),
}, opts); err != nil {
return nil, err
}
startupScript, err := os.ReadFile(bootstrapPath)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", bootstrapPath, err)
}
imageGitea := fmt.Sprintf("%s/%s/%s/gitea:prod", cfg.ARHost(), cfg.Project, registry.RepoID)
imageCaddy := fmt.Sprintf("%s/%s/%s/caddy:prod", cfg.ARHost(), cfg.Project, registry.RepoID)
metadata := pulumi.StringMap{
// OS Login means SSH access follows IAM instead of metadata keys.
"enable-oslogin": pulumi.String("TRUE"),
"google-logging-enabled": pulumi.String("true"),
"config-bucket": buckets.Name,
"backup-bucket": backupBucket.Name,
"gcp-project": pulumi.String(cfg.Project),
"gcp-region": pulumi.String(cfg.Region),
"ar-host": pulumi.String(cfg.ARHost()),
// Floating :prod tags -- AutoUpdate=registry has nothing to poll if
// these are digests.
"image-gitea": pulumi.String(imageGitea),
"image-caddy": pulumi.String(imageCaddy),
"domain": pulumi.String(cfg.Domain),
"app-name": pulumi.String(cfg.AppName),
"require-signin-view": pulumi.String(strconv.FormatBool(cfg.RequireSigninView)),
// Drives Coraza's SecRuleEngine and, from the same value, whether
// the fail2ban jail acting on WAF verdicts is enabled.
"waf-mode": pulumi.String(cfg.WAFMode),
"podman-subnet": pulumi.String(cfg.PodmanCIDR),
"podman-gateway": pulumi.String(gatewayFor(cfg.PodmanCIDR)),
"data-disk-device": pulumi.String("/dev/disk/by-id/google-" + DataDiskDeviceName),
// Not read by the guest -- it exists so `gcloud compute instances
// describe` shows which revision of vm/ the box was built against.
"config-hash": pulumi.String(configHash),
}
if net.Runners != nil {
// The Cloud NAT address every GKE runner and CI job egresses from.
// vm/bootstrap.sh puts it in fail2ban's ignoreip: one misconfigured job
// retrying a bad token would otherwise ban all of CI at once. Omitted,
// not empty, when runners are off -- meta() already returns "" on 404.
metadata["runner-egress-ip"] = net.Runners.EgressIP.Address
}
vm, err := gcpcompute.NewInstance(ctx, "gitea-vm", &gcpcompute.InstanceArgs{
Name: pulumi.String("gitea-vm"),
Zone: pulumi.String(cfg.Zone),
MachineType: pulumi.String(cfg.MachineType),
Tags: pulumi.ToStringArray([]string{network.Tag}),
Description: pulumi.String("Gitea, Caddy, and podman quadlets on AlmaLinux 10"),
BootDisk: &gcpcompute.InstanceBootDiskArgs{
InitializeParams: &gcpcompute.InstanceBootDiskInitializeParamsArgs{
// Image family, not a pinned image: new VMs pick up the monthly
// refreshed AlmaLinux build automatically.
Image: pulumi.String("almalinux-cloud/almalinux-10"),
Size: pulumi.Int(cfg.BootDiskGB),
Type: pulumi.String("pd-balanced"),
},
},
AttachedDisks: gcpcompute.InstanceAttachedDiskArray{
&gcpcompute.InstanceAttachedDiskArgs{
Source: disk.ID(),
DeviceName: pulumi.String(DataDiskDeviceName),
Mode: pulumi.String("READ_WRITE"),
},
},
NetworkInterfaces: gcpcompute.InstanceNetworkInterfaceArray{
&gcpcompute.InstanceNetworkInterfaceArgs{
Network: net.Network.ID(),
Subnetwork: net.Subnetwork.ID(),
AccessConfigs: gcpcompute.InstanceNetworkInterfaceAccessConfigArray{
&gcpcompute.InstanceNetworkInterfaceAccessConfigArgs{
NatIp: net.Address.Address,
},
},
},
},
ServiceAccount: &gcpcompute.InstanceServiceAccountArgs{
Email: sa.Email,
// cloud-platform plus per-resource IAM, rather than a hand-rolled
// scope list: scopes are a coarse legacy filter and IAM is where the
// real restrictions live.
Scopes: pulumi.ToStringArray([]string{"cloud-platform"}),
},
ShieldedInstanceConfig: &gcpcompute.InstanceShieldedInstanceConfigArgs{
EnableSecureBoot: pulumi.Bool(true),
EnableVtpm: pulumi.Bool(true),
EnableIntegrityMonitoring: pulumi.Bool(true),
},
Metadata: metadata,
MetadataStartupScript: pulumi.String(string(startupScript)),
// Lets Pulumi change machine type or metadata in place instead of
// refusing the update.
AllowStoppingForUpdate: pulumi.Bool(true),
Labels: pulumi.StringMap{"app": pulumi.String("gitea")},
}, opts)
if err != nil {
return nil, err
}
return &Instance{VM: vm, Disk: disk}, nil
}
// gatewayFor returns the .1 address of a /24. The podman network's gateway has
// to be stated explicitly alongside the pinned subnet.
func gatewayFor(cidr string) string {
var a, b, c, d, bits int
if _, err := fmt.Sscanf(cidr, "%d.%d.%d.%d/%d", &a, &b, &c, &d, &bits); err != nil {
return ""
}
return fmt.Sprintf("%d.%d.%d.1", a, b, c)
}