A Go-based infrastructure “monorepo” powered by Pulumi, with configuration stored as HJSON and decoded into a protobuf schema.
[!NOTE] This repository is the operational source for several personal infrastructure areas. Prefer previews and narrow area changes before applying updates.
This repository manages multiple infrastructure “areas”:
area/apps: Kubernetes applications deployed to a cluster.area/cf: Cloudflare resources (zones, DNS records, R2 buckets/custom domains).area/do: DigitalOcean resources (VPC + Kubernetes clusters).area/gh: GitHub repositories and settings (branch protection, Pages, collaborators).area/k8s: Cluster add-ons installed via helm/kubectl (Makefile driven, not Pulumi).Each Pulumi area has:
area/<name>/main.goarea/<name>/<name>.hjsonarea/<name>/Pulumi.yamlShared implementation lives under internal/ (e.g. internal/app, internal/cf, internal/do, internal/gh).
Development and CI-style commands use:
go.mod)makebin/buf for protobuf linting, breaking-change checks, and code generationfieldalignment for make lintgolangci-lint for make lint when installed in PATHshellcheck for make scripts-lintgotestsum for make specsgovulncheck and Trivy for make secInfrastructure operation also uses:
Operator-only helper targets use these tools as needed:
make -C area/apps save-config and make -C area/k8s save-config: doctlmake -C area/apps setup|delete|rollout: kubectlmake -C area/apps lint: kube-score, kubescape, kubectl (live lean namespace scan)make -C area/apps verify: curlmake -C area/apps load: vegetamake -C area/k8s setup|delete|pods: helm, kubectlAll area configuration files are HJSON (*.hjson). They are decoded into protobuf messages defined in:
api/infraops/v2/service.protoThose protobuf messages are then converted into internal Go types and used to provision resources.
The protobuf types provide the expected configuration shape. Semantic checks that are not modeled in the schema may still fail later during Pulumi or provider operations.
The top-level version field in each HJSON file is the configuration schema version used by this
repo’s tooling. It is not a Kubernetes, provider, Pulumi, or application runtime version.
This repo includes a small CLI to normalize/format config files by:
Build:
make build-format
Format a config kind (apps|cf|do|gh), using the default location area/<kind>/<kind>.hjson:
./format -k cf
Override the path:
./format -k cf -p area/cf/cf.hjson
The selected kind must match the file’s area-specific fields. A mismatch is rejected without changing the file.
A few conventions are implemented by the Go code and are worth knowing when editing HJSON:
Application.kind Deployment Model (apps)Supported values:
"api": uses image tag docker.io/alexfalkowski/<name>:v<version>, mounts the app config file and listed secret volumes, injects SERVICE_ID, runs the container with server, exposes debug 6060, HTTP 8080, and gRPC 9090, and creates a Service/Ingress so the application receives inbound traffic."worker": identical to "api" except no Service or Ingress is created, for an application that does not receive inbound traffic (for example a queue consumer or background job).Other values are unsupported and are not pre-validated by the helper code; malformed values may fail later during Pulumi/Kubernetes application.
All apps receive CircleCI release-agent labels/annotations and must serve HTTP liveness/readiness
probes at /<name>/livez and /<name>/readyz on port 8080; startup uses a TCP probe on 8080.
All app containers use native lifecycle.preStop.sleep for five seconds before Kubernetes sends the
termination signal, allowing endpoint and routing updates to propagate. Their 35-second pod termination
grace period leaves 30 seconds for graceful process shutdown after the delay.
Application.id CircleCI Project ID (apps)Application.id is written to the deployment annotation circleci.com/project-id. Keep it aligned
with the CircleCI project identifier when adding or renaming apps.
EnvVar.value Secret References (apps)Environment variables support literal values, and a secret reference format:
secret:<secretName>/<key>At deploy time, this becomes a Kubernetes SecretKeyRef:
<secretName>-secret<key>The format is intentionally not pre-validated by the helper code; malformed references are expected to fail during Pulumi/Kubernetes application.
Example:
env_vars: [
{ name: "DATABASE_URL", value: "secret:db/url" }
]
Application.secrets vs Secret Env Vars (apps)Application.secrets is an application-level dependency list used by the deployment implementation to wire existing Kubernetes Secrets as volumes.env_vars (the secret:<secretName>/<key> format) reference specific keys in those secrets.<secretName> values, but they serve different purposes.<secretName>-secret and is mounted at /etc/secrets/<secretName>.Application.resource Sizing (apps)Application.resource selects a resource profile. Current mapping:
"small" (default): cpu 125m-250m, memory 64Mi-128Mi, ephemeral-storage 1Gi-2GiUnknown values fall back to "small".
Application.replicas Pod Count (apps)Always set replicas explicitly; there is no fallback to three. Because this is a plain proto3
scalar, omission decodes to zero. Use replicas: 0 explicitly when the application should run no pods.
Canonical rewrites by the format or bump tool may omit an explicit zero; that does not change the
deployed replica count.
replicas: 3
[!WARNING]
replicas: 1is valid, but the current rolling update and PodDisruptionBudget both allow one unavailable pod, so it does not guarantee continuous availability during disruption or rollout.
The apps Pulumi program creates a NetworkPolicy that selects each app’s pods. Ingress is limited
to debug on 6060, HTTP on 8080, and gRPC on 9090 for every app, regardless of kind. Egress
currently remains open because outbound traffic flows are not modeled per app yet. Future egress
restrictions should be introduced per namespace/app after the required flows are known.
The root Makefile includes shared build tooling from the bin submodule. Initialize it before running Make targets from a fresh checkout:
git submodule sync
git submodule update --init
[!IMPORTANT] Run
make depbefore local validation after checkout, dependency changes, or generated/vendor-state changes.
From the repository root:
make dep # download/tidy/vendor deps
make lint # lint Go sources (including field alignment)
make scripts-lint # lint repository shell workflow helpers
make sec # govulncheck + Trivy
make specs # gotestsum + go test (junit/coverage under test/reports)
make coverage # HTML + function coverage under test/reports
[!IMPORTANT] Do not edit generated Go code under
api/infraops/v2/*.pb.godirectly.
Instead:
make api-lint
make api-breaking
make api-generate
(Or: make -C api lint|breaking|generate.)
make api-breaking compares the API module against the GitHub master branch, so it needs network
access to run outside CI.
Pulumi is typically run via Makefile targets from the repo root.
Login:
make pulumi-login
Pulumi also needs the provider credentials for the area being previewed or updated:
| Area | Required local access |
|---|---|
apps |
Kubernetes access through the current kubeconfig context |
cf |
CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID |
do |
DIGITALOCEAN_TOKEN |
gh |
GITHUB_TOKEN |
Preview/update:
make area=cf pulumi-preview
make area=cf pulumi-update
Supported areas for these targets:
apps, cf, do, ghThe Makefile runs Pulumi with:
alexfalkowski/<area>/prodarea/<area>That working directory matters because the programs read <area>.hjson via a relative path.
Other stack operations:
make area=cf pulumi-refresh
make area=cf pulumi-cancel
make area=cf pulumi-delete
[!WARNING]
pulumi-update,pulumi-refresh, andpulumi-cancelaffect remote infrastructure or stack state. The Make targets pass--yes, so treat the preview as the confirmation step unless you are recovering from a known failed operation.
[!CAUTION]
pulumi-deleterunspulumi stack rm --force; it removes Pulumi stack state and can orphan managed resources if they still exist.
area/apps)Deploys Kubernetes applications described in area/apps/apps.hjson.
See:
area/apps/apps.hjsonThis file uses the Kubernetes message in api/infraops/v2/service.proto.
Apps also need an application config file at:
area/apps/<namespace>/<app>.yamlFor example, the bezeichner app in namespace lean reads area/apps/lean/bezeichner.yaml.
The apps Pulumi program turns that file into a Kubernetes ConfigMap entry named <app>.yaml,
then mounts it into the container at /etc/<app>/<app>.yaml.
Only applications listed in area/apps/apps.hjson are deployed. The tracked files under
area/apps/fiction/ are example app configs and are not used unless a matching app entry references
that namespace/name.
Adding an application to apps.hjson only wires the Pulumi resources. To include it in release
rollout, verify, load, and kubescape workflows, also update area/apps/release with the app name and its
per-app helper commands, including any request payload fixture needed by load
testing.
The abgleich app is a worker, so it has no Service, Ingress, or public DNS record. Its verify
helper waits up to 120 seconds for Kubernetes rollout readiness, and its load helper skips HTTP
load testing. Kubernetes still probes the worker’s internal HTTP liveness and readiness endpoints.
Save the cluster kubeconfig if your local context is not already configured:
make -C area/apps save-config
Prepare the local app namespace/helper resources:
make -C area/apps setup
Before deploying Abgleich, create abgleich-secret in the existing lean namespace
from a local GitHub token file. github_token_file must be a nonempty file path:
make -C area/apps abgleich-secret github_token_file=/absolute/path/to/github-token
This explicit creation step is separate from setup and requires kubectl with the
intended cluster context. It creates the github-token key using --from-file without
printing its contents; creation fails if the Secret already exists. Abgleich’s
secrets list mounts it read-only at /etc/secrets/abgleich/github-token.
area/apps/lean/abgleich.yaml reads that file through github.token and configures
the worker to reconcile auto-merge pull requests in alexfalkowski/infraops every
five minutes. The token needs pull request write access and permission to update
repository contents. Credentials are read once at startup; restart the worker
after replacing the token.
Apply the Pulumi resources described by apps.hjson:
make area=apps pulumi-update
The default rollout, verify, load, and kubescape helper targets are release-aware. They inspect the app
release diff and target only apps with version-only changes in area/apps/apps.hjson; when the diff
is not a release-only app change, they fall back to all supported apps.
Supported apps are the apps explicitly wired in area/apps/release. The API verify helpers use
fail-fast curl requests with bounded connection and total request time, and the API load helpers run
Vegeta and fail unless every request succeeds. Load request payloads live with the app fixtures under
area/apps/<namespace>/.
The release helper compares APPS_RELEASE_BASE to APPS_RELEASE_HEAD when those environment
variables are set. Otherwise it uses the current commit’s parent, falling back to origin/master
when the parent is unavailable. In CI, area/apps/release halt-ci halts the general build for a
release-only apps version bump so the apps update workflow can run the targeted Pulumi update,
verification, load, and live-resource lint path. Set APPS_RELEASE_DRY_RUN=true to print the
release helper action instead of running it.
make -C area/apps rollout
make -C area/apps verify
make -C area/apps load
Use the *-all targets when you want to cover every supported app explicitly:
make -C area/apps rollout-all
make -C area/apps verify-all
make -C area/apps load-all
The load* targets write ignored vegeta result files under area/apps/lean/*.bin.
Per-app commands are exposed by the release helper:
cd area/apps
./release verify-web
./release load-bezeichner
./release rollout-standort
./release kubescape-web
The lint target requires a working cluster context. kube-score always scans live resources from
the lean namespace, while kubescape follows the release-aware workflow: it scans each changed app
Deployment and its resolved container image for release-only version changes and otherwise scans the
whole namespace. The kube-score helper intentionally ignores the host pod anti-affinity check and
fails when Kubernetes discovery or manifest collection returns no live resources.
[!CAUTION] This deletes the
leannamespace throughkubectl.
make -C area/apps delete
Build:
make build-bump
Update a single app version in config. This is an internal helper used by automation, and the
-v value is expected to be a semantic version:
./bump -n bezeichner -v <version>
By default it edits area/apps/apps.hjson. Override path:
./bump -n bezeichner -v <version> -p area/apps/apps.hjson
[!TIP] Run
./format -k appsafter edits to keep config normalized.
area/cf)Manages Cloudflare resources using Pulumi’s Cloudflare provider:
Config:
area/cf/cf.hjsonThe shared Cloudflare zone baseline enables HTTPS, the code-defined minimum TLS setting, aggressive caching, HTTP/3, and HTTP/2 prioritization, disables email obfuscation, and applies the SSL mode for the zone model.
balancer_zones create a Cloudflare zone plus proxied A records for each configured
record_names entry under the zone domain. When ipv6 is configured, they also create proxied
AAAA records. Balancer zones use the shared baseline with SSL mode full.
balancer_zones: [
{
name: example
domain: example.com
ipv4: 203.0.113.10
ipv6: 2001:db8::10
record_names: [
api
web
]
}
]
page_zones create a Cloudflare zone plus a proxied www.<domain> CNAME to host. Page zones use
the shared baseline with SSL mode strict.
page_zones: [
{
name: docs
domain: example.com
host: example.github.io
}
]
R2 buckets are optional. Omit zone to create only the bucket. Include zone to create an enabled
R2 custom domain using the code-defined TLS minimum:
buckets: [
{
name: assets
region: EEUR
zone: {
id: "<cloudflare-zone-id>"
domain: assets.example.com
}
}
]
Cloudflare Pulumi runs require:
CLOUDFLARE_API_TOKEN for provider authenticationCLOUDFLARE_ACCOUNT_ID for account-scoped resources like R2 bucketsarea/do)Manages DigitalOcean resources using Pulumi’s DigitalOcean provider:
Config:
area/do/do.hjsonThe DigitalOcean program creates the cluster VPC and then provisions the cluster with fixed operational defaults:
fra1223:00Cluster resource values map to node capacity:
"small" (default): 2 vCPU / 4 GB node"medium": 4 vCPU / 8 GB nodeUnknown or empty values fall back to "small".
Some items may be created manually depending on account setup:
| Name | Description |
|---|---|
| lean-thoughts | All experiments for lean-thoughts. |
The Pulumi program creates the VPC used by the cluster and attaches the cluster to it.
Cluster version is pinned in code:
internal/do/do.goGuidance:
area/gh)Manages GitHub resources using Pulumi’s GitHub provider:
Config:
area/gh/gh.hjsonThis area was inspired by:
Every managed repository receives a fixed baseline in addition to the HJSON fields: merge commits and rebase merges are disabled, squash merge, auto-merge, update branch, auto-init, branch deletion on merge, issues, projects, wiki, secret scanning, push protection, and vulnerability alerts are enabled, and web commit signoff is disabled. These settings are intentional and are not configurable in HJSON.
Every non-archived managed repository also receives an automerge label with color 0E8A16
and description “Eligible for automated merging.” Archived repositories are read-only and are
skipped for label creation.
template creates a repository from another GitHub template repository and requires both owner and
repository. is_template is separate: it marks the resulting repository as a reusable template.
is_template: true
template: {
owner: alexfalkowski
repository: go-service-template
}
Every repository must list at least one checks entry. Those values become the required status
check contexts for strict branch protection on master; an empty list fails during provisioning.
Branch protection also enforces linear history and intentionally requires zero approving PR reviews because required status checks are the primary merge gate for this solo-maintainer workflow.
Some repository features may require a two-step approach: create the repository first, then enable features in a follow-up change. This avoids timing issues around initial default branch creation.
First change: disable Pages (or omit pages config):
pages: { enabled: false }
Second change: enable Pages:
pages: { enabled: true }
Optional CNAME:
pages: {
enabled: true
cname: www.yoursite.com
}
Pages uses legacy GitHub Pages sourced from master; branch, path, and build type are not
configurable in HJSON.
When enabled, collaborator management grants push permission to lean-thoughts-ci
on alexfalkowski/<repository>.
First change:
collaborators: { enabled: false }
Second change:
collaborators: { enabled: true }
If the pipeline fails due to timing, a rerun often succeeds.
area/k8s)This is not a Pulumi area. It contains cluster add-ons installed via helm/kubectl.
These targets are intended to be run manually from an operator workstation, not from CI.
[!CAUTION] Run this only after you have a Kubernetes cluster, a kubeconfig context for that cluster, and the required local secrets.
Save the cluster kubeconfig if your local context is not already configured:
make -C area/k8s save-config
Setup:
make -C area/k8s setup
The nginx ingress add-on uses the Helm values in area/k8s/nginx/values.yaml. Those values restrict
the DigitalOcean load balancer allow rules to Cloudflare CIDR ranges, so the ingress is intended for
Cloudflare-proxied origin traffic. Direct clients or non-Cloudflare DNS paths may be blocked at the
load balancer.
Delete:
[!CAUTION] This deletes the
nginx-ingress,circleci, andmetrics-servernamespaces throughkubectl.
make -C area/k8s delete
Useful debugging:
make -C area/k8s pods
The default make -C area/k8s setup path installs the CircleCI release agent along with nginx ingress and metrics-server.
[!IMPORTANT]
CIRCLECI_K8S_TOKENis required when running the default k8s setup target.
CIRCLECI_K8S_TOKEN (CircleCI release agent)Because the Makefile is a local-operator workflow, the CircleCI release-agent token is passed directly to Helm rather than through a CI secret-handling path.
area/: Pulumi programs and k8s add-onsinternal/: shared implementation (convert + create patterns per area)api/: protobuf schema and generated codecmd/: small helper CLIs (format, bump)bin/: shared build tooling (git submodule)