feat: organize all services into separate stacks by dependency

This commit is contained in:
2025-11-22 17:51:58 +05:00
parent 06a316f1e6
commit a25c25afc4
30 changed files with 2513 additions and 386 deletions

View File

@@ -1,48 +0,0 @@
import * as fs from "fs";
import { Construct } from "constructs";
import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
type OnePasswordSecret = {
id?: string;
namespace?: string;
name: string;
itemPath: string;
};
type OnePasswordOptions = {
provider: KubernetesProvider;
namespace: string;
};
export class OnePassword extends Construct {
constructor(scope: Construct, id: string, options: OnePasswordOptions) {
super(scope, id);
const secrets: OnePasswordSecret[] = JSON.parse(
fs.readFileSync("1password/secrets.json", {
encoding: "utf8",
}),
);
secrets.forEach((secret) => {
new Manifest(this, secret.id ?? secret.name, {
provider: options.provider,
manifest: {
apiVersion: "onepassword.com/v1",
kind: "OnePasswordItem",
metadata: {
name: secret.name,
namespace: secret.namespace ?? options.namespace,
annotations: {
"operator.1password.io/auto-restart": "true",
},
},
spec: {
itemPath: secret.itemPath,
},
},
});
});
}
}

View File

@@ -1,60 +0,0 @@
[
{
"name": "gitea-admin",
"itemPath": "vaults/Lab/items/gitea-admin"
},
{
"name": "pihole-admin",
"itemPath": "vaults/Lab/items/pihole"
},
{
"name": "postgres-password",
"itemPath": "vaults/Lab/items/Postgres"
},
{
"name": "runner-secret",
"itemPath": "vaults/Lab/items/Gitea"
},
{
"name": "cloudflare-token",
"itemPath": "vaults/Lab/items/cloudflare"
},
{
"id": "cloudflare-token-longhorn",
"name": "cloudflare-token",
"itemPath": "vaults/Lab/items/cloudflare",
"namespace": "longhorn-system"
},
{
"name": "valkey",
"itemPath": "vaults/Lab/items/valkey"
},
{
"name": "gitea-oauth",
"itemPath": "vaults/Lab/items/gitea-oauth"
},
{
"name": "gitea-elasticsearch",
"itemPath": "vaults/Lab/items/gitea-elasticsearch"
},
{
"name": "smtp-token",
"itemPath": "vaults/Lab/items/smtp-token"
},
{
"name": "longhorn-encryption",
"itemPath": "vaults/Lab/items/longhorn-encryption"
},
{
"name": "authentik-secret-key",
"itemPath": "vaults/Lab/items/authentik-secret-key"
},
{
"name": "curseforge",
"itemPath": "vaults/Lab/items/curseforge"
},
{
"name": "devpi-secret",
"itemPath": "vaults/Lab/items/devpi-secret"
}
]

View File

@@ -1,28 +0,0 @@
import * as fs from "fs";
import { HelmProvider } from "@cdktf/provider-helm/lib/provider";
import { Release } from "@cdktf/provider-helm/lib/release";
import { Construct } from "constructs";
type AuthentikServerOptions = {
provider: HelmProvider;
name: string;
namespace: string;
};
export class AuthentikServer extends Construct {
constructor(scope: Construct, id: string, options: AuthentikServerOptions) {
super(scope, id);
new Release(this, id, {
...options,
repository: "https://charts.goauthentik.io",
chart: "authentik",
createNamespace: true,
values: [
fs.readFileSync("helm/values/authentik.values.yaml", {
encoding: "utf8",
}),
],
});
}
}

1085
barman.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,17 @@
import { Construct } from "constructs";
import { TerraformStack } from "cdktf";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
import { NixCache } from "./nixcache";
export class CacheInfrastructure extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
const kubernetes = new KubernetesProvider(this, "kubernetes", {
configPath: "~/.kube/config",
});
// Add cache-related infrastructure components here
new NixCache(this, "nix-cache", kubernetes);
}
}

View File

@@ -1,23 +1,18 @@
import * as fs from "fs"; import * as fs from "fs";
import * as path from "path"; import * as path from "path";
import { Construct } from "constructs"; import { Construct } from "constructs";
import { TerraformStack } from "cdktf";
import { PersistentVolumeClaimV1 } from "@cdktf/provider-kubernetes/lib/persistent-volume-claim-v1"; import { PersistentVolumeClaimV1 } from "@cdktf/provider-kubernetes/lib/persistent-volume-claim-v1";
import { ConfigMapV1 } from "@cdktf/provider-kubernetes/lib/config-map-v1"; import { ConfigMapV1 } from "@cdktf/provider-kubernetes/lib/config-map-v1";
import { DeploymentV1 } from "@cdktf/provider-kubernetes/lib/deployment-v1"; import { DeploymentV1 } from "@cdktf/provider-kubernetes/lib/deployment-v1";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
import { TraefikIngressRoute } from "../traefik/ingress-route"; import { IngressRoute } from "../../utils";
import { ServiceV1 } from "@cdktf/provider-kubernetes/lib/service-v1"; import { ServiceV1 } from "@cdktf/provider-kubernetes/lib/service-v1";
export class NixCache extends TerraformStack { export class NixCache extends Construct {
constructor(scope: Construct, id: string) { constructor(scope: Construct, id: string, kubernetes: KubernetesProvider) {
super(scope, id); super(scope, id);
const kubernetes = new KubernetesProvider(this, "kubernetes", {
configPath: "~/.kube/config",
});
const pvc = new PersistentVolumeClaimV1(this, "pvc", { const pvc = new PersistentVolumeClaimV1(this, "pvc", {
provider: kubernetes, provider: kubernetes,
metadata: { metadata: {
@@ -134,7 +129,7 @@ export class NixCache extends TerraformStack {
}, },
}); });
new TraefikIngressRoute(this, "ingress-route", { new IngressRoute(this, "ingress-route", {
provider: kubernetes, provider: kubernetes,
namespace: "homelab", namespace: "homelab",
host: "nix.dogar.dev", host: "nix.dogar.dev",

View File

@@ -1,69 +0,0 @@
import * as fs from "fs";
import { HelmProvider } from "@cdktf/provider-helm/lib/provider";
import { Release } from "@cdktf/provider-helm/lib/release";
import { Construct } from "constructs";
import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
type GiteaServerOptions = {
providers: {
helm: HelmProvider;
kubernetes: KubernetesProvider;
};
name: string;
namespace: string;
r2Endpoint: string;
};
export class GiteaServer extends Construct {
constructor(scope: Construct, id: string, options: GiteaServerOptions) {
super(scope, id);
const { kubernetes, helm } = options.providers;
new Release(this, id, {
...options,
provider: helm,
repository: "https://dl.gitea.com/charts",
chart: "gitea",
createNamespace: true,
set: [
{
name: "gitea.config.storage.MINIO_ENDPOINT",
value: options.r2Endpoint,
},
],
values: [
fs.readFileSync("helm/values/gitea.values.yaml", {
encoding: "utf8",
}),
],
});
new Manifest(this, `${id}-ssh-ingress`, {
provider: kubernetes,
manifest: {
apiVersion: "traefik.io/v1alpha1",
kind: "IngressRouteTCP",
metadata: {
name: "gitea-ssh-ingress",
namespace: options.namespace,
},
spec: {
entryPoints: ["ssh"],
routes: [
{
match: "HostSNI(`*`)",
services: [
{
name: `${options.name}-ssh`,
port: 22,
},
],
},
],
},
},
});
}
}

42
k8s-operators/barman.ts Normal file
View File

@@ -0,0 +1,42 @@
import { Construct } from "constructs";
import { NullProvider } from "@cdktf/provider-null/lib/provider";
import { Resource } from "@cdktf/provider-null/lib/resource";
export interface BarmanCloudPluginInstallOptions {
/** URL to the CloudNativePG barman-cloud plugin manifest */
url: string;
}
export class BarmanCloudPluginInstall extends Construct {
constructor(
scope: Construct,
id: string,
opts: BarmanCloudPluginInstallOptions,
) {
super(scope, id);
const { url } = opts;
const applyCmd = ["kubectl", "apply", "-f", url].join(" ");
const deleteCmd = ["kubectl", "delete", "-f", url].join(" ");
new Resource(this, "barman-install", {
provider: new NullProvider(this, "barman"),
provisioners: [
{
type: "local-exec",
when: "create",
command: applyCmd,
},
{
type: "local-exec",
when: "destroy",
command: deleteCmd,
},
],
triggers: {
url,
},
});
}
}

72
k8s-operators/index.ts Normal file
View File

@@ -0,0 +1,72 @@
import * as fs from "fs";
import * as path from "path";
import { HelmProvider } from "@cdktf/provider-helm/lib/provider";
import { Release } from "@cdktf/provider-helm/lib/release";
import { TerraformStack } from "cdktf";
import { Construct } from "constructs";
import { BarmanCloudPluginInstall } from "./barman";
import { Prometheus } from "./prometheus";
export class K8SOperators extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
const helm = new HelmProvider(this, "helm", {
kubernetes: {
configPath: "~/.kube/config",
},
});
new Prometheus(this, "prometheus", {
provider: helm,
namespace: "monitoring",
name: "prometheus-operator",
version: "75.10.0",
});
new Release(this, "onepassword-operator", {
provider: helm,
name: "onepassword-operator",
chart: "connect",
repository: "https://1password.github.io/connect-helm-charts/",
namespace: "1password",
createNamespace: true,
set: [
{
name: "operator.create",
value: "true",
},
],
setSensitive: [
{
name: "operator.token.value",
value: process.env.OP_CONNECT_TOKEN!,
},
{
name: "connect.credentials_base64",
value: btoa(
fs.readFileSync(
path.join(__dirname, "1password-credentials.json"),
"utf-8",
),
),
},
],
});
const cnpg = new Release(this, "cnpg-operator", {
provider: helm,
repository: "https://cloudnative-pg.github.io/charts",
chart: "cloudnative-pg",
name: "postgres-system",
namespace: "cnpg-system",
createNamespace: true,
});
const barman = new BarmanCloudPluginInstall(this, "barman-cloud-plugin", {
url: "https://github.com/cloudnative-pg/plugin-barman-cloud/releases/download/v0.9.0/manifest.yaml",
});
barman.node.addDependency(cnpg);
}
}

113
main.ts
View File

@@ -1,35 +1,27 @@
import * as dotenv from "dotenv"; import * as dotenv from "dotenv";
import { cleanEnv, str } from "envalid"; import { cleanEnv, str } from "envalid";
import { Construct } from "constructs"; import { Construct } from "constructs";
import { App, TerraformStack, LocalBackend, PgBackend } from "cdktf"; import { App, TerraformStack, LocalBackend, TerraformOutput } from "cdktf";
import { HelmProvider } from "@cdktf/provider-helm/lib/provider"; import { HelmProvider } from "@cdktf/provider-helm/lib/provider";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
import { NamespaceV1 } from "@cdktf/provider-kubernetes/lib/namespace-v1"; import { NamespaceV1 } from "@cdktf/provider-kubernetes/lib/namespace-v1";
import { GiteaServer } from "./gitea";
import { OnePassword } from "./1password";
import { PostgresCluster } from "./postgres";
import { Longhorn } from "./longhorn"; import { Longhorn } from "./longhorn";
import { AuthentikServer } from "./authentik";
import { ValkeyCluster } from "./valkey";
import { CertManager } from "./cert-manager"; import { CertManager } from "./cert-manager";
import { Traefik } from "./traefik"; import { Traefik } from "./traefik";
import { Prometheus } from "./prometheus";
import { MetalLB } from "./metallb"; import { MetalLB } from "./metallb";
import { NixCache } from "./nixcache"; import { CacheInfrastructure } from "./cache-infrastructure";
import { UtilityServices } from "./utility-services";
import { K8SOperators } from "./k8s-operators";
dotenv.config(); dotenv.config();
const env = cleanEnv(process.env, { cleanEnv(process.env, {
ACCOUNT_ID: str({ desc: "Cloudflare account id." }), ACCOUNT_ID: str({ desc: "Cloudflare account id." }),
PG_CONN_STR: str({ OP_CONNECT_TOKEN: str({ desc: "1Password Connect token." }),
desc: "PostgreSQL connection string for Terraform state backend.",
}),
}); });
const r2Endpoint = `https://${env.ACCOUNT_ID}.r2.cloudflarestorage.com`; class CoreServices extends TerraformStack {
class Homelab extends TerraformStack {
constructor(scope: Construct, id: string) { constructor(scope: Construct, id: string) {
super(scope, id); super(scope, id);
@@ -52,6 +44,10 @@ class Homelab extends TerraformStack {
}, },
}); });
new TerraformOutput(this, "namespace-output", {
value: namespace,
});
new Longhorn(this, "longhorn", { new Longhorn(this, "longhorn", {
name: "longhorn", name: "longhorn",
providers: { providers: {
@@ -66,21 +62,14 @@ class Homelab extends TerraformStack {
namespace: "metallb-system", namespace: "metallb-system",
}); });
new OnePassword(this, "one-password", {
provider: kubernetes,
namespace,
});
new Traefik(this, "traefik", { new Traefik(this, "traefik", {
provider: helm, provider: helm,
namespace, namespace,
name: "traefik", name: "traefik",
}); });
const certManagerApiVersion = "cert-manager.io/v1";
new CertManager(this, "cert-manager", { new CertManager(this, "cert-manager", {
certManagerApiVersion, certManagerApiVersion: "cert-manager.io/v1",
name: "cert-manager", name: "cert-manager",
namespace, namespace,
version: "1.18.2", version: "1.18.2",
@@ -89,71 +78,39 @@ class Homelab extends TerraformStack {
helm, helm,
}, },
}); });
new Prometheus(this, "prometheus", {
provider: helm,
namespace,
name: "prometheus-operator",
version: "75.10.0",
});
const pg = new PostgresCluster(this, "postgres-cluster", {
certManagerApiVersion,
name: "postgres-cluster",
namespace,
providers: {
kubernetes,
helm,
},
users: ["shahab", "budget-tracker", "authentik", "gitea"],
primaryUser: "shahab",
initSecretName: "postgres-password",
backupR2EndpointURL: r2Endpoint,
});
const valkey = new ValkeyCluster(this, "valkey-cluster", {
provider: kubernetes,
namespace,
name: "valkey",
});
const authentik = new AuthentikServer(this, "authentik-server", {
provider: helm,
name: "authentik",
namespace,
});
authentik.node.addDependency(pg);
authentik.node.addDependency(valkey);
const gitea = new GiteaServer(this, "gitea-server", {
name: "gitea",
namespace,
providers: {
helm,
kubernetes,
},
r2Endpoint: `${env.ACCOUNT_ID}.r2.cloudflarestorage.com`,
});
gitea.node.addDependency(authentik);
} }
} }
const app = new App(); const app = new App();
const homelab = new Homelab(app, "homelab"); const coreServices = new CoreServices(app, "homelab");
const nixCache = new NixCache(app, "nix-cache"); const k8sOperators = new K8SOperators(app, "k8s-operators");
nixCache.node.addDependency(homelab); k8sOperators.node.addDependency(coreServices);
new LocalBackend(homelab, { const utilityServices = new UtilityServices(app, "utility-services");
utilityServices.node.addDependency(k8sOperators);
const caches = new CacheInfrastructure(app, "cache-infrastructure");
caches.node.addDependency(utilityServices);
new LocalBackend(coreServices, {
path: "terraform.tfstate", path: "terraform.tfstate",
workspaceDir: ".", workspaceDir: ".",
}); });
new PgBackend(nixCache, { new LocalBackend(caches, {
schemaName: "nix_cache", path: "terraform.tfstate",
connStr: env.PG_CONN_STR, workspaceDir: "./cachestf",
});
new LocalBackend(utilityServices, {
path: "terraform.tfstate",
workspaceDir: "./utilityservicestf",
});
new LocalBackend(k8sOperators, {
path: "terraform.tfstate",
workspaceDir: "./k8soperatorstf",
}); });
app.synth(); app.synth();

124
package-lock.json generated
View File

@@ -9,46 +9,60 @@
"version": "1.0.0", "version": "1.0.0",
"license": "GPL-3.0-or-later", "license": "GPL-3.0-or-later",
"dependencies": { "dependencies": {
"@cdktf/provider-helm": "10.5.0", "@cdktf/provider-helm": "12.1.1",
"@cdktf/provider-kubernetes": "11.12.1", "@cdktf/provider-kubernetes": "12.1.0",
"cdktf": "^0.20.12", "@cdktf/provider-null": "^11.0.0",
"constructs": "^10.4.2", "cdktf": "^0.21.0",
"dotenv": "^16.5.0", "constructs": "^10.4.3",
"envalid": "^8.0.0" "dotenv": "^17.2.3",
"envalid": "^8.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.3", "@types/node": "^24.10.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.9.3"
}, },
"engines": { "engines": {
"node": "24" "node": "24"
} }
}, },
"node_modules/@cdktf/provider-helm": { "node_modules/@cdktf/provider-helm": {
"version": "10.5.0", "version": "12.1.1",
"resolved": "https://registry.npmjs.org/@cdktf/provider-helm/-/provider-helm-10.5.0.tgz", "resolved": "https://registry.npmjs.org/@cdktf/provider-helm/-/provider-helm-12.1.1.tgz",
"integrity": "sha512-u3Q6VNIayaSFfEKZh+JG3PDrwcl9igHLWUdi6cK1G385tw4UyUpZ8osUnGhOErxbZtlcp4yeZ1c5+1OMP4epLA==", "integrity": "sha512-bi1Smig+b38eKs0yP/JJhbwTKHclp91fNLkcEDS7nI+6AQ4+uqN24CxHGUc6hpwNmatNnLH90gR0+iq/p6KEuw==",
"license": "MPL-2.0", "license": "MPL-2.0",
"engines": { "engines": {
"node": ">= 18.12.0" "node": ">= 20.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"cdktf": "^0.20.0", "cdktf": "^0.21.0",
"constructs": "^10.3.0" "constructs": "^10.4.2"
} }
}, },
"node_modules/@cdktf/provider-kubernetes": { "node_modules/@cdktf/provider-kubernetes": {
"version": "11.12.1", "version": "12.1.0",
"resolved": "https://registry.npmjs.org/@cdktf/provider-kubernetes/-/provider-kubernetes-11.12.1.tgz", "resolved": "https://registry.npmjs.org/@cdktf/provider-kubernetes/-/provider-kubernetes-12.1.0.tgz",
"integrity": "sha512-8LgaY0VULF/2f8iXqojGujP7DKSzl1didqbxMb7uMX0oE3EVDdtmJNIAY2D6oXjW95b9+NVQmhg4iN/jiF7zpA==", "integrity": "sha512-GVFbQIPaMeGbzbGyvTWwBUgdc9kKOGWRQNmzvD5A1bFtDTAVk77kRfdfooVuj869TDHF77WXIn6LGp8uuHoJrQ==",
"license": "MPL-2.0", "license": "MPL-2.0",
"engines": { "engines": {
"node": ">= 18.12.0" "node": ">= 20.9.0"
}, },
"peerDependencies": { "peerDependencies": {
"cdktf": "^0.20.0", "cdktf": "^0.21.0",
"constructs": "^10.3.0" "constructs": "^10.4.2"
}
},
"node_modules/@cdktf/provider-null": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/@cdktf/provider-null/-/provider-null-11.0.0.tgz",
"integrity": "sha512-OX/ADMXtPWBV/ZTBxCiMGUX0EMI+ooxXZWrZAskJAKIO2Ny1tpBXLE13NpfU9fG+6GkR4e1hLNsOMdO99DuhCA==",
"license": "MPL-2.0",
"engines": {
"node": ">= 20.9.0"
},
"peerDependencies": {
"cdktf": "^0.21.0",
"constructs": "^10.4.2"
} }
}, },
"node_modules/@cspotcode/source-map-support": { "node_modules/@cspotcode/source-map-support": {
@@ -93,9 +107,9 @@
} }
}, },
"node_modules/@tsconfig/node10": { "node_modules/@tsconfig/node10": {
"version": "1.0.11", "version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@@ -121,9 +135,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.9.1", "version": "24.10.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true, "peer": true,
@@ -165,9 +179,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/cdktf": { "node_modules/cdktf": {
"version": "0.20.12", "version": "0.21.0",
"resolved": "https://registry.npmjs.org/cdktf/-/cdktf-0.20.12.tgz", "resolved": "https://registry.npmjs.org/cdktf/-/cdktf-0.21.0.tgz",
"integrity": "sha512-ZBg2gA3Uw0WvGFlgrY1uxo6QHWn+ZdHiDkZQyOsTBl68k62UlaV8K7RR51d0E/amQG/CjtKOJr5XPFFAcOq0VA==", "integrity": "sha512-bdTOOyrFSXw0p5d7/3dye7ZWYzrUatyMjWEAAwTGoqghjygRj6Q55y1QZnSB021NRDzYZ3BhFGsOkpmIjQMzNQ==",
"bundleDependencies": [ "bundleDependencies": [
"archiver", "archiver",
"json-stable-stringify", "json-stable-stringify",
@@ -177,11 +191,11 @@
"peer": true, "peer": true,
"dependencies": { "dependencies": {
"archiver": "7.0.1", "archiver": "7.0.1",
"json-stable-stringify": "1.2.1", "json-stable-stringify": "1.3.0",
"semver": "7.7.1" "semver": "7.7.2"
}, },
"peerDependencies": { "peerDependencies": {
"constructs": "^10.3.0" "constructs": "^10.4.2"
} }
}, },
"node_modules/cdktf/node_modules/@isaacs/cliui": { "node_modules/cdktf/node_modules/@isaacs/cliui": {
@@ -766,12 +780,12 @@
} }
}, },
"node_modules/cdktf/node_modules/json-stable-stringify": { "node_modules/cdktf/node_modules/json-stable-stringify": {
"version": "1.2.1", "version": "1.3.0",
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"call-bind": "^1.0.8", "call-bind": "^1.0.8",
"call-bound": "^1.0.3", "call-bound": "^1.0.4",
"isarray": "^2.0.5", "isarray": "^2.0.5",
"jsonify": "^0.0.1", "jsonify": "^0.0.1",
"object-keys": "^1.1.1" "object-keys": "^1.1.1"
@@ -852,6 +866,17 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/cdktf/node_modules/minimatch": {
"version": "5.1.6",
"inBundle": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cdktf/node_modules/minipass": { "node_modules/cdktf/node_modules/minipass": {
"version": "7.1.2", "version": "7.1.2",
"inBundle": true, "inBundle": true,
@@ -940,17 +965,6 @@
"minimatch": "^5.1.0" "minimatch": "^5.1.0"
} }
}, },
"node_modules/cdktf/node_modules/readdir-glob/node_modules/minimatch": {
"version": "5.1.6",
"inBundle": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=10"
}
},
"node_modules/cdktf/node_modules/safe-buffer": { "node_modules/cdktf/node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"funding": [ "funding": [
@@ -971,7 +985,7 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/cdktf/node_modules/semver": { "node_modules/cdktf/node_modules/semver": {
"version": "7.7.1", "version": "7.7.2",
"inBundle": true, "inBundle": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@@ -1249,9 +1263,9 @@
} }
}, },
"node_modules/constructs": { "node_modules/constructs": {
"version": "10.4.2", "version": "10.4.3",
"resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.3.tgz",
"integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", "integrity": "sha512-3+ZB67qWGM1vEstNpj6pGaLNN1qz4gxC1CBhEUhZDZk0PqzQWY65IzC1Doq17MGPa9xa2wJ1G/DJ3swU8kWAHQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true "peer": true
}, },
@@ -1273,9 +1287,9 @@
} }
}, },
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.6.1", "version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=12" "node": ">=12"
@@ -1285,9 +1299,9 @@
} }
}, },
"node_modules/envalid": { "node_modules/envalid": {
"version": "8.1.0", "version": "8.1.1",
"resolved": "https://registry.npmjs.org/envalid/-/envalid-8.1.0.tgz", "resolved": "https://registry.npmjs.org/envalid/-/envalid-8.1.1.tgz",
"integrity": "sha512-OT6+qVhKVyCidaGoXflb2iK1tC8pd0OV2Q+v9n33wNhUJ+lus+rJobUj4vJaQBPxPZ0vYrPGuxdrenyCAIJcow==", "integrity": "sha512-vOUfHxAFFvkBjbVQbBfgnCO9d3GcNfMMTtVfgqSU2rQGMFEVqWy9GBuoSfHnwGu7EqR0/GeukQcL3KjFBaga9w==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"tslib": "2.8.1" "tslib": "2.8.1"

View File

@@ -23,16 +23,17 @@
"upgrade:next": "npm i cdktf@next cdktf-cli@next" "upgrade:next": "npm i cdktf@next cdktf-cli@next"
}, },
"dependencies": { "dependencies": {
"@cdktf/provider-helm": "10.5.0", "@cdktf/provider-helm": "12.1.1",
"@cdktf/provider-kubernetes": "11.12.1", "@cdktf/provider-kubernetes": "12.1.0",
"cdktf": "^0.20.12", "@cdktf/provider-null": "^11.0.0",
"constructs": "^10.4.2", "cdktf": "^0.21.0",
"dotenv": "^16.5.0", "constructs": "^10.4.3",
"envalid": "^8.0.0" "dotenv": "^17.2.3",
"envalid": "^8.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^24.0.3", "@types/node": "^24.10.1",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "^5.8.3" "typescript": "^5.9.3"
} }
} }

View File

@@ -1,14 +1,9 @@
import { HelmProvider } from "@cdktf/provider-helm/lib/provider";
import { Release } from "@cdktf/provider-helm/lib/release";
import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest"; import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
import { Construct } from "constructs"; import { Construct } from "constructs";
type PostgresClusterOptions = { type PostgresClusterOptions = {
providers: { provider: KubernetesProvider;
kubernetes: KubernetesProvider;
helm: HelmProvider;
};
name: string; name: string;
namespace: string; namespace: string;
users: string[]; users: string[];
@@ -22,16 +17,7 @@ export class PostgresCluster extends Construct {
constructor(scope: Construct, id: string, options: PostgresClusterOptions) { constructor(scope: Construct, id: string, options: PostgresClusterOptions) {
super(scope, id); super(scope, id);
const { kubernetes, helm } = options.providers; const { provider } = options;
new Release(this, "cnpg-operator", {
provider: helm,
repository: "https://cloudnative-pg.github.io/charts",
chart: "cloudnative-pg",
name: "postgres-system",
namespace: "cnpg-system",
createNamespace: true,
});
const destinationPath = "s3://postgres-backups/"; const destinationPath = "s3://postgres-backups/";
const endpointURL = options.backupR2EndpointURL; const endpointURL = options.backupR2EndpointURL;
@@ -64,7 +50,7 @@ export class PostgresCluster extends Construct {
}; };
new Manifest(this, "r2-backup-store", { new Manifest(this, "r2-backup-store", {
provider: kubernetes, provider,
manifest: { manifest: {
apiVersion: "barmancloud.cnpg.io/v1", apiVersion: "barmancloud.cnpg.io/v1",
kind: "ObjectStore", kind: "ObjectStore",
@@ -95,7 +81,7 @@ export class PostgresCluster extends Construct {
// Self-signed issuer for creating CA certificates // Self-signed issuer for creating CA certificates
new Manifest(this, "selfsigned-issuer", { new Manifest(this, "selfsigned-issuer", {
provider: kubernetes, provider,
manifest: { manifest: {
apiVersion: certManagerApiVersion, apiVersion: certManagerApiVersion,
kind: "Issuer", kind: "Issuer",
@@ -111,7 +97,7 @@ export class PostgresCluster extends Construct {
// Server CA certificate // Server CA certificate
new Manifest(this, "server-ca-cert", { new Manifest(this, "server-ca-cert", {
provider: kubernetes, provider,
manifest: { manifest: {
apiVersion: certManagerApiVersion, apiVersion: certManagerApiVersion,
kind: "Certificate", kind: "Certificate",
@@ -140,7 +126,7 @@ export class PostgresCluster extends Construct {
// Issuer using the server CA // Issuer using the server CA
new Manifest(this, "server-ca-issuer", { new Manifest(this, "server-ca-issuer", {
provider: kubernetes, provider,
manifest: { manifest: {
apiVersion: certManagerApiVersion, apiVersion: certManagerApiVersion,
kind: "Issuer", kind: "Issuer",
@@ -158,7 +144,7 @@ export class PostgresCluster extends Construct {
// Server certificate // Server certificate
new Manifest(this, "server-cert", { new Manifest(this, "server-cert", {
provider: kubernetes, provider,
manifest: { manifest: {
apiVersion: certManagerApiVersion, apiVersion: certManagerApiVersion,
kind: "Certificate", kind: "Certificate",
@@ -187,7 +173,7 @@ export class PostgresCluster extends Construct {
// Client CA certificate // Client CA certificate
new Manifest(this, "client-ca", { new Manifest(this, "client-ca", {
provider: kubernetes, provider,
manifest: { manifest: {
apiVersion: certManagerApiVersion, apiVersion: certManagerApiVersion,
kind: "Certificate", kind: "Certificate",
@@ -216,7 +202,7 @@ export class PostgresCluster extends Construct {
// Issuer using the client CA // Issuer using the client CA
new Manifest(this, "client-ca-issuer", { new Manifest(this, "client-ca-issuer", {
provider: kubernetes, provider,
manifest: { manifest: {
apiVersion: certManagerApiVersion, apiVersion: certManagerApiVersion,
kind: "Issuer", kind: "Issuer",
@@ -234,7 +220,7 @@ export class PostgresCluster extends Construct {
// Secret for client certificate // Secret for client certificate
new Manifest(this, `${certNames.client}-secret`, { new Manifest(this, `${certNames.client}-secret`, {
provider: kubernetes, provider,
manifest: { manifest: {
apiVersion: "v1", apiVersion: "v1",
kind: "Secret", kind: "Secret",
@@ -250,7 +236,7 @@ export class PostgresCluster extends Construct {
// Client certificate for streaming replica // Client certificate for streaming replica
new Manifest(this, "streaming-replica-cert", { new Manifest(this, "streaming-replica-cert", {
provider: kubernetes, provider,
manifest: { manifest: {
apiVersion: certManagerApiVersion, apiVersion: certManagerApiVersion,
kind: "Certificate", kind: "Certificate",
@@ -277,7 +263,7 @@ export class PostgresCluster extends Construct {
options.users.forEach( options.users.forEach(
(user) => (user) =>
new Manifest(this, `${user}-client-cert`, { new Manifest(this, `${user}-client-cert`, {
provider: kubernetes, provider,
manifest: { manifest: {
apiVersion: certManagerApiVersion, apiVersion: certManagerApiVersion,
kind: "Certificate", kind: "Certificate",
@@ -302,7 +288,7 @@ export class PostgresCluster extends Construct {
); );
new Manifest(this, "postgres-cluster", { new Manifest(this, "postgres-cluster", {
provider: kubernetes, provider,
fieldManager: { forceConflicts: true }, fieldManager: { forceConflicts: true },
manifest: { manifest: {
apiVersion: "postgresql.cnpg.io/v1", apiVersion: "postgresql.cnpg.io/v1",
@@ -435,7 +421,7 @@ export class PostgresCluster extends Construct {
}); });
new Manifest(this, "postgres-backup-job", { new Manifest(this, "postgres-backup-job", {
provider: kubernetes, provider,
manifest: { manifest: {
apiVersion: "postgresql.cnpg.io/v1", apiVersion: "postgresql.cnpg.io/v1",
kind: "ScheduledBackup", kind: "ScheduledBackup",

7
types/index.ts Normal file
View File

@@ -0,0 +1,7 @@
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
import { HelmProvider } from "@cdktf/provider-helm/lib/provider";
export type Providers = {
kubernetes: KubernetesProvider;
helm: HelmProvider;
};

View File

@@ -0,0 +1,47 @@
import * as fs from "fs";
import * as path from "path";
import { Release } from "@cdktf/provider-helm/lib/release";
import { Construct } from "constructs";
import { OnePasswordSecret } from "../../utils";
import { Providers } from "../../types";
type AuthentikServerOptions = {
providers: Providers;
name: string;
namespace: string;
};
export class AuthentikServer extends Construct {
constructor(scope: Construct, id: string, options: AuthentikServerOptions) {
super(scope, id);
const { kubernetes, helm } = options.providers;
new OnePasswordSecret(this, "secret-key", {
provider: kubernetes,
name: "authentik-secret-key",
namespace: options.namespace,
itemPath: "vaults/Lab/items/authentik-secret-key",
});
new OnePasswordSecret(this, "smtp", {
provider: kubernetes,
name: "authentik-smtp-token",
namespace: options.namespace,
itemPath: "vaults/Lab/items/smtp-token",
});
new Release(this, id, {
...options,
provider: helm,
repository: "https://charts.goauthentik.io",
chart: "authentik",
createNamespace: true,
values: [
fs.readFileSync(path.join(__dirname, "values.yaml"), {
encoding: "utf8",
}),
],
}).importFrom("homelab/authentik");
}
}

View File

@@ -0,0 +1,110 @@
global:
addPrometheusAnnotations: true
securityContext:
runAsUser: 1000
fsGroup: 1000
podLabels:
app: authentik
nodeSelector:
nodepool: worker
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app: authentik
env:
- name: AUTHENTIK_SECRET_KEY
valueFrom:
secretKeyRef:
name: authentik-secret-key
key: password
- name: AUTHENTIK_EMAIL__USERNAME
valueFrom:
secretKeyRef:
name: authentik-smtp-token
key: authentik-username
- name: AUTHENTIK_EMAIL__PASSWORD
valueFrom:
secretKeyRef:
name: authentik-smtp-token
key: authentik-password
- name: AUTHENTIK_EMAIL__FROM
valueFrom:
secretKeyRef:
name: authentik-smtp-token
key: authentik-username
- name: AUTHENTIK_EMAIL__USE_TLS
value: "true"
- name: AUTHENTIK_POSTGRESQL__SSLMODE
value: verify-full
- name: AUTHENTIK_POSTGRESQL__SSLROOTCERT
value: "/opt/authentik/certs/ca.crt"
- name: AUTHENTIK_POSTGRESQL__SSLCERT
value: "/opt/authentik/certs/tls.crt"
- name: AUTHENTIK_POSTGRESQL__SSLKEY
value: "/opt/authentik/certs/tls.key"
- name: AUTHENTIK_REDIS__PASSWORD
valueFrom:
secretKeyRef:
name: valkey
key: password
volumes:
- name: ssl-bundle
projected:
sources:
- secret:
name: authentik-client-cert
items:
- key: tls.crt
path: tls.crt
- key: tls.key
path: tls.key
mode: 0600
- secret:
name: postgres-server-cert
items:
- key: ca.crt
path: ca.crt
volumeMounts:
- name: ssl-bundle
mountPath: /opt/authentik/certs
readOnly: true
authentik:
error_reporting:
enabled: false
email:
host: "smtp.protonmail.ch"
port: 587
postgresql:
host: postgres-cluster-rw
user: authentik
name: authentik
redis:
host: valkey
server:
replicas: 3
ingress:
enabled: true
annotations:
cert-manager.io/cluster-issuer: cloudflare-issuer
cert-manager.io/acme-challenge-type: dns01
cert-manager.io/private-key-size: "4096"
ingressClassName: traefik
hosts:
- auth.dogar.dev
tls:
- secretName: authentik-tls
hosts:
- auth.dogar.dev
worker:
replicas: 3
postgresql:
enabled: false
redis:
enabled: false

View File

@@ -0,0 +1 @@
export { GiteaServer } from "./server";

View File

@@ -0,0 +1,82 @@
import * as fs from "fs";
import * as path from "path";
import { HelmProvider } from "@cdktf/provider-helm/lib/provider";
import { Release } from "@cdktf/provider-helm/lib/release";
import { Construct } from "constructs";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
import { OnePasswordSecret } from "../../utils";
import { IngressRouteTcp } from "../../utils/traefik";
type GiteaServerOptions = {
providers: {
helm: HelmProvider;
kubernetes: KubernetesProvider;
};
name: string;
namespace: string;
r2Endpoint: string;
};
export class GiteaServer extends Construct {
constructor(scope: Construct, id: string, options: GiteaServerOptions) {
super(scope, id);
const { kubernetes, helm } = options.providers;
new OnePasswordSecret(this, "admin", {
provider: kubernetes,
name: "gitea-admin",
namespace: options.namespace,
itemPath: "vaults/Lab/items/gitea-admin",
});
new OnePasswordSecret(this, "oauth", {
provider: kubernetes,
name: "gitea-oauth",
namespace: options.namespace,
itemPath: "vaults/Lab/items/gitea-oauth",
});
new OnePasswordSecret(this, "smtp", {
provider: kubernetes,
name: "gitea-smtp-token",
namespace: options.namespace,
itemPath: "vaults/Lab/items/smtp-token",
});
new OnePasswordSecret(this, "r2", {
provider: kubernetes,
name: "gitea-cloudflare-token",
namespace: options.namespace,
itemPath: "vaults/Lab/items/cloudflare",
});
new Release(this, id, {
...options,
provider: helm,
repository: "https://dl.gitea.com/charts",
chart: "gitea",
createNamespace: true,
set: [
{
name: "gitea.config.storage.MINIO_ENDPOINT",
value: options.r2Endpoint,
},
],
values: [
fs.readFileSync(path.join(__dirname, "values.yaml"), {
encoding: "utf8",
}),
],
});
new IngressRouteTcp(this, "ssh-ingress", {
provider: kubernetes,
namespace: options.namespace,
entryPoint: "ssh",
serviceName: `${options.name}-ssh`,
servicePort: 22,
});
}
}

View File

@@ -0,0 +1,161 @@
global:
storageClass: longhorn
image:
rootless: false
service:
http:
annotations:
metallb.universe.tf/allow-shared-ip: gitea
ssh:
annotations:
metallb.universe.tf/allow-shared-ip: gitea
ingress:
enabled: true
annotations:
cert-manager.io/cluster-issuer: cloudflare-issuer
cert-manager.io/acme-challenge-type: dns01
cert-manager.io/private-key-size: 4096
className: traefik
hosts:
- host: git.dogar.dev
paths:
- path: /
pathType: Prefix
tls:
- secretName: gitea-tls
hosts:
- git.dogar.dev
gitea:
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "6060"
admin:
existingSecret: gitea-admin
metrics:
enabled: true
serviceMonitor:
enabled: true
config:
server:
ENABLE_PPROF: true
ENABLE_GZIP: true
LFS_START_SERVER: true
SSH_DOMAIN: git.dogar.dev
database:
DB_TYPE: postgres
HOST: postgres-cluster-rw
NAME: gitea
USER: gitea
SSL_MODE: verify-full
metrics:
ENABLED: true
cache:
ADAPTER: memory
session:
PROVIDER: db
PROVIDER_CONFIG: ""
queue:
TYPE: channel
storage:
STORAGE_TYPE: minio
MINIO_USE_SSL: true
MINIO_BUCKET_LOOKUP_STYLE: path
MINIO_LOCATION: auto
service:
DISABLE_REGISTRATION: true
oauth2_client:
ENABLE_AUTO_REGISTRATION: true
mailer:
ENABLED: true
PROTOCOL: smtp+starttls
SMTP_ADDR: smtp.protonmail.ch
SMTP_PORT: 587
FROM: git@dogar.dev
picture:
GRAVATAR_SOURCE: gravatar
oauth:
- name: "authentik"
provider: "openidConnect"
existingSecret: gitea-oauth
autoDiscoverUrl: "https://auth.dogar.dev/application/o/gitea/.well-known/openid-configuration"
iconUrl: "https://goauthentik.io/img/icon.png"
scopes: "email profile"
additionalConfigFromEnvs:
- name: GITEA__MAILER__PASSWD
valueFrom:
secretKeyRef:
name: gitea-smtp-token
key: gitea-password
- name: GITEA__PACKAGES__CHUNKED_UPLOAD_PATH
value: "/tmp/gitea-uploads"
- name: GITEA__PACKAGES__CHUNKED_UPLOAD_CONCURRENCY
value: "4"
- name: GITEA__STORAGE__MINIO_ACCESS_KEY_ID
valueFrom:
secretKeyRef:
name: gitea-cloudflare-token
key: access_key_id
- name: GITEA__STORAGE__MINIO_SECRET_ACCESS_KEY
valueFrom:
secretKeyRef:
name: gitea-cloudflare-token
key: secret_access_key
persistence:
labels:
recurring-job.longhorn.io/source: "enabled"
recurring-job.longhorn.io/daily-backup: "enabled"
enabled: true
size: 50Gi
accessModes:
- ReadWriteMany
deployment:
env:
- name: PGSSLMODE
value: verify-full
- name: PGSSLROOTCERT
value: /opt/gitea/.postgresql/root.crt
- name: PGSSLCERT
value: /opt/gitea/.postgresql/postgresql.crt
- name: PGSSLKEY
value: /opt/gitea/.postgresql/postgresql.key
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 6
memory: 6Gi
extraVolumes:
- name: ssl-bundle
projected:
sources:
- secret:
name: gitea-client-cert
items:
- key: tls.crt
path: postgresql.crt
- key: tls.key
path: postgresql.key
mode: 0600
- secret:
name: postgres-server-cert
items:
- key: ca.crt
path: root.crt
- name: gitea-temp
emptyDir: {}
extraInitVolumeMounts:
- name: ssl-bundle
mountPath: /opt/gitea/.postgresql
readOnly: true
extraContainerVolumeMounts:
- name: ssl-bundle
mountPath: /opt/gitea/.postgresql
readOnly: true
readOnly: true
- name: gitea-temp
mountPath: /tmp/gitea-uploads
postgresql-ha:
enabled: false
valkey-cluster:
enabled: false

94
utility-services/index.ts Normal file
View File

@@ -0,0 +1,94 @@
import * as path from "path";
import { DataKubernetesNamespaceV1 } from "@cdktf/provider-kubernetes/lib/data-kubernetes-namespace-v1";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
import { HelmProvider } from "@cdktf/provider-helm/lib/provider";
import { DataTerraformRemoteStateLocal, TerraformStack } from "cdktf";
import { Construct } from "constructs";
import { ValkeyCluster } from "./valkey";
import { GiteaServer } from "./gitea";
import { AuthentikServer } from "./authentik";
import { PostgresCluster } from "./postgres";
export class UtilityServices extends TerraformStack {
constructor(scope: Construct, id: string) {
super(scope, id);
const kubernetes = new KubernetesProvider(this, "kubernetes", {
configPath: "~/.kube/config",
});
const helm = new HelmProvider(this, "helm", {
kubernetes: {
configPath: "~/.kube/config",
},
});
const homelabState = new DataTerraformRemoteStateLocal(
this,
"homelab-state",
{
path: path.join(
__dirname,
"../cdktf.out/stacks/homelab/terraform.tfstate",
),
},
);
const namespaceName = homelabState.getString("namespace-output");
const namespaceResource = new DataKubernetesNamespaceV1(
this,
"homelab-namespace",
{
provider: kubernetes,
metadata: {
name: namespaceName,
},
},
);
const namespace = namespaceResource.metadata.name;
const r2Endpoint = `${process.env.ACCOUNT_ID!}.r2.cloudflarestorage.com`;
const valkeyCluster = new ValkeyCluster(this, "valkey-cluster", {
namespace,
provider: kubernetes,
name: "valkey",
});
const postgres = new PostgresCluster(this, "postgres-cluster", {
certManagerApiVersion: "cert-manager.io/v1",
name: "postgres-cluster",
namespace,
provider: kubernetes,
users: ["shahab", "budget-tracker", "authentik", "gitea"],
primaryUser: "shahab",
initSecretName: "postgres-password",
backupR2EndpointURL: `https://${r2Endpoint}`,
});
const authentik = new AuthentikServer(this, "authentik-server", {
providers: {
helm,
kubernetes,
},
name: "authentik",
namespace,
});
authentik.node.addDependency(valkeyCluster);
authentik.node.addDependency(postgres);
const gitea = new GiteaServer(this, "gitea-server", {
providers: {
helm,
kubernetes,
},
name: "gitea",
namespace,
r2Endpoint: r2Endpoint,
});
gitea.node.addDependency(authentik);
}
}

View File

@@ -0,0 +1,456 @@
import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
import { Construct } from "constructs";
import { OnePasswordSecret } from "../../utils";
type PostgresClusterOptions = {
provider: KubernetesProvider;
name: string;
namespace: string;
users: string[];
primaryUser: string;
initSecretName: string;
certManagerApiVersion: string;
backupR2EndpointURL: string;
};
export class PostgresCluster extends Construct {
constructor(scope: Construct, id: string, options: PostgresClusterOptions) {
super(scope, id);
const { provider } = options;
const destinationPath = "s3://postgres-backups/";
const endpointURL = options.backupR2EndpointURL;
const barmanStoreName = "r2-postgres-backup-store";
const backupServerName = `${options.name}-backup`;
const barmanConfiguration = {
destinationPath,
endpointURL,
s3Credentials: {
accessKeyId: {
name: "barman-cloudflare-token",
key: "access_key_id",
},
secretAccessKey: {
name: "barman-cloudflare-token",
key: "secret_access_key",
},
region: {
name: "barman-cloudflare-token",
key: "AWS_REGION",
},
},
wal: {
compression: "gzip",
},
data: {
compression: "gzip",
},
};
new OnePasswordSecret(this, "barman-cloudflare-token", {
provider: options.provider,
name: "barman-cloudflare-token",
namespace: options.namespace,
itemPath: "vaults/Lab/items/cloudflare",
});
new Manifest(this, "r2-backup-store", {
provider,
manifest: {
apiVersion: "barmancloud.cnpg.io/v1",
kind: "ObjectStore",
metadata: {
namespace: options.namespace,
name: barmanStoreName,
},
spec: {
retentionPolicy: "15d",
configuration: {
...barmanConfiguration,
},
},
},
});
const { certManagerApiVersion } = options;
const certNames = {
server: "postgres-server-cert",
client: "postgres-client-cert",
};
const caNames = {
server: "postgres-server-ca",
client: "postgres-client-ca",
};
// Self-signed issuer for creating CA certificates
new Manifest(this, "selfsigned-issuer", {
provider,
manifest: {
apiVersion: certManagerApiVersion,
kind: "Issuer",
metadata: {
name: "selfsigned-issuer",
namespace: options.namespace,
},
spec: {
selfSigned: {},
},
},
});
// Server CA certificate
new Manifest(this, "server-ca-cert", {
provider,
manifest: {
apiVersion: certManagerApiVersion,
kind: "Certificate",
metadata: {
name: "server-ca",
namespace: options.namespace,
},
spec: {
isCA: true,
commonName: caNames.server,
secretName: caNames.server,
privateKey: {
algorithm: "ECDSA",
size: 384,
},
duration: "52560h", // 6 years
renewBefore: "8760h", // 1 year before expiration
issuerRef: {
name: "selfsigned-issuer",
kind: "Issuer",
group: "cert-manager.io",
},
},
},
});
// Issuer using the server CA
new Manifest(this, "server-ca-issuer", {
provider,
manifest: {
apiVersion: certManagerApiVersion,
kind: "Issuer",
metadata: {
name: `${caNames.server}-issuer`,
namespace: options.namespace,
},
spec: {
ca: {
secretName: caNames.server,
},
},
},
});
// Server certificate
new Manifest(this, "server-cert", {
provider,
manifest: {
apiVersion: certManagerApiVersion,
kind: "Certificate",
metadata: {
name: certNames.server,
namespace: options.namespace,
},
spec: {
secretName: certNames.server,
usages: ["server auth"],
dnsNames: [
"postgres-cluster-rw",
"postgres-cluster-rw.homelab.svc.cluster.local",
"postgres.dogar.dev",
],
duration: "4380h", // 6 months
renewBefore: "720h", // 30 days before expiration
issuerRef: {
name: `${caNames.server}-issuer`,
kind: "Issuer",
group: "cert-manager.io",
},
},
},
});
// Client CA certificate
new Manifest(this, "client-ca", {
provider,
manifest: {
apiVersion: certManagerApiVersion,
kind: "Certificate",
metadata: {
name: "client-ca",
namespace: options.namespace,
},
spec: {
isCA: true,
commonName: caNames.client,
secretName: caNames.client,
privateKey: {
algorithm: "ECDSA",
size: 256,
},
duration: "52560h", // 6 years
renewBefore: "8760h", // 1 year before expiration
issuerRef: {
name: "selfsigned-issuer",
kind: "Issuer",
group: "cert-manager.io",
},
},
},
});
// Issuer using the client CA
new Manifest(this, "client-ca-issuer", {
provider,
manifest: {
apiVersion: certManagerApiVersion,
kind: "Issuer",
metadata: {
name: `${caNames.client}-issuer`,
namespace: options.namespace,
},
spec: {
ca: {
secretName: caNames.client,
},
},
},
});
// Secret for client certificate
new Manifest(this, `${certNames.client}-secret`, {
provider,
manifest: {
apiVersion: "v1",
kind: "Secret",
metadata: {
name: certNames.client,
namespace: options.namespace,
labels: {
"cnpg.io/reload": "",
},
},
},
});
// Client certificate for streaming replica
new Manifest(this, "streaming-replica-cert", {
provider,
manifest: {
apiVersion: certManagerApiVersion,
kind: "Certificate",
metadata: {
name: certNames.client,
namespace: options.namespace,
},
spec: {
secretName: certNames.client,
usages: ["client auth"],
commonName: "streaming_replica",
duration: "4380h", // 6 months
renewBefore: "720h", // 30 days before expiration
issuerRef: {
name: "postgres-client-ca-issuer",
kind: "Issuer",
group: "cert-manager.io",
},
},
},
});
// Client certificates for users
options.users.forEach(
(user) =>
new Manifest(this, `${user}-client-cert`, {
provider,
manifest: {
apiVersion: certManagerApiVersion,
kind: "Certificate",
metadata: {
name: `${user}-client-cert`,
namespace: options.namespace,
},
spec: {
secretName: `${user}-client-cert`,
usages: ["client auth"],
commonName: user,
duration: "4380h", // 6 months
renewBefore: "720h", // 30 days before expiration
issuerRef: {
name: "postgres-client-ca-issuer",
kind: "Issuer",
group: "cert-manager.io",
},
},
},
}),
);
new Manifest(this, "postgres-cluster", {
provider,
fieldManager: { forceConflicts: true },
manifest: {
apiVersion: "postgresql.cnpg.io/v1",
kind: "Cluster",
metadata: {
name: options.name,
namespace: options.namespace,
},
spec: {
instances: 3,
minSyncReplicas: 1,
maxSyncReplicas: 2,
primaryUpdateStrategy: "unsupervised",
certificates: {
serverCASecret: certNames.server,
serverTLSSecret: certNames.server,
clientCASecret: certNames.client,
replicationTLSSecret: certNames.client,
},
postgresql: {
parameters: {
archive_mode: "on",
archive_timeout: "60min",
checkpoint_timeout: "10min",
checkpoint_completion_target: "0.7",
dynamic_shared_memory_type: "posix",
full_page_writes: "on",
log_destination: "csvlog",
log_directory: "/controller/log",
log_filename: "postgres",
log_rotation_age: "0",
log_rotation_size: "0",
log_truncate_on_rotation: "false",
logging_collector: "on",
max_parallel_workers: "32",
max_replication_slots: "32",
max_worker_processes: "32",
max_slot_wal_keep_size: "256MB",
max_wal_size: "512MB",
min_wal_size: "128MB",
shared_memory_type: "mmap",
shared_preload_libraries: "",
ssl_max_protocol_version: "TLSv1.3",
ssl_min_protocol_version: "TLSv1.3",
wal_compression: "on",
wal_keep_size: "128MB",
wal_level: "replica",
wal_log_hints: "on",
wal_receiver_timeout: "5s",
wal_sender_timeout: "5s",
},
pg_hba: [
`hostssl all ${options.primaryUser} all cert`,
"hostssl sameuser all all cert",
],
},
plugins: [
{
name: "barman-cloud.cloudnative-pg.io",
isWALArchiver: true,
parameters: {
barmanObjectName: barmanStoreName,
serverName: backupServerName,
},
},
],
bootstrap: {
recovery: {
source: "clusterBackup",
},
},
externalClusters: [
{
name: "clusterBackup",
plugin: {
name: "barman-cloud.cloudnative-pg.io",
parameters: {
barmanObjectName: barmanStoreName,
serverName: backupServerName,
skipWalArchiveCheck: true,
},
},
},
],
managed: {
services: {
disabledDefaultServices: ["ro", "r"],
additional: [
{
selectorType: "rw",
serviceTemplate: {
metadata: {
name: "postgres-cluster",
superuser: true,
},
spec: {
type: "LoadBalancer",
},
},
},
],
},
roles: [
{
name: options.primaryUser,
inRoles: ["postgres"],
inherit: true,
disablePassword: true,
createdb: true,
createrole: true,
login: true,
ensure: "present",
},
],
},
storage: {
size: "10Gi",
storageClass: "longhorn",
},
walStorage: {
size: "2Gi",
storageClass: "longhorn",
},
},
},
});
new Manifest(this, "postgres-backup-job", {
provider,
manifest: {
apiVersion: "postgresql.cnpg.io/v1",
kind: "ScheduledBackup",
metadata: {
name: "postgres-cluster",
namespace: options.namespace,
},
spec: {
immediate: true,
// weekly midnight on Sunday
schedule: "* 0 0 * * 0",
backupOwnerReference: "self",
method: "plugin",
pluginConfiguration: {
name: "barman-cloud.cloudnative-pg.io",
parameters: {
barmanObjectName: barmanStoreName,
serverName: backupServerName,
},
},
cluster: {
name: options.name,
},
},
},
});
}
}

View File

@@ -2,6 +2,7 @@ import { DeploymentV1 } from "@cdktf/provider-kubernetes/lib/deployment-v1";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider"; import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
import { ServiceV1 } from "@cdktf/provider-kubernetes/lib/service-v1"; import { ServiceV1 } from "@cdktf/provider-kubernetes/lib/service-v1";
import { Construct } from "constructs"; import { Construct } from "constructs";
import { OnePasswordSecret } from "../../utils";
type ValkeyClusterOptions = { type ValkeyClusterOptions = {
provider: KubernetesProvider; provider: KubernetesProvider;
@@ -17,6 +18,13 @@ export class ValkeyCluster extends Construct {
const labels = { app: "valkey" }; const labels = { app: "valkey" };
const { provider, name, namespace } = options; const { provider, name, namespace } = options;
new OnePasswordSecret(this, "valkey-secret", {
provider,
name: "valkey",
namespace,
itemPath: "vaults/Lab/items/valkey",
});
new DeploymentV1(this, "valkey-deployment", { new DeploymentV1(this, "valkey-deployment", {
provider, provider,
metadata: { metadata: {

View File

@@ -0,0 +1,36 @@
import { Construct } from "constructs";
import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
type SecretOptions = {
provider: KubernetesProvider;
namespace: string;
name: string;
itemPath: string;
};
export class OnePasswordSecret extends Construct {
constructor(scope: Construct, id: string, options: SecretOptions) {
super(scope, id);
const { itemPath, name, namespace, provider } = options;
new Manifest(this, name, {
provider,
manifest: {
apiVersion: "onepassword.com/v1",
kind: "OnePasswordItem",
metadata: {
name,
namespace,
annotations: {
"operator.1password.io/auto-restart": "true",
},
},
spec: {
itemPath,
},
},
});
}
}

View File

@@ -0,0 +1,85 @@
import { Construct } from "constructs";
import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
export interface CertificateOptions {
provider: KubernetesProvider;
/** Namespace to create the Certificate in */
namespace: string;
/** Required name of the certificate (and CRD name) */
name: string;
/** Secret name for storing the issued TLS cert */
secretName: string;
/** One or more DNS names the certificate should cover */
dnsNames: string[];
/** Reference to the cert-manager issuer */
issuerRef: {
name: string;
kind?: string; // ClusterIssuer or Issuer
};
/** Optional duration (default: cert-manager default) */
duration?: string;
/** Optional renewBefore (default: cert-manager default) */
renewBefore?: string;
}
class Certificate extends Construct {
public readonly manifest: Manifest;
constructor(scope: Construct, id: string, opts: CertificateOptions) {
super(scope, id);
const manifest: any = {
apiVersion: "cert-manager.io/v1",
kind: "Certificate",
metadata: {
name: opts.name,
namespace: opts.namespace,
},
spec: {
secretName: opts.secretName,
dnsNames: opts.dnsNames,
issuerRef: {
name: opts.issuerRef.name,
kind: opts.issuerRef.kind ?? "ClusterIssuer",
},
},
};
if (opts.duration) {
manifest.spec.duration = opts.duration;
}
if (opts.renewBefore) {
manifest.spec.renewBefore = opts.renewBefore;
}
this.manifest = new Manifest(this, id, {
provider: opts.provider,
manifest,
});
}
}
export class CloudflareCertificate extends Certificate {
constructor(
scope: Construct,
id: string,
opts: Omit<CertificateOptions, "issuerRef">,
) {
super(scope, id, {
...opts,
issuerRef: {
name: "cloudflare-issuer",
kind: "ClusterIssuer",
},
});
}
}

3
utils/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export { CloudflareCertificate } from "./cert-manager";
export { OnePasswordSecret } from "./1password-secret";
export { IngressRoute } from "./traefik";

2
utils/traefik/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export { IngressRoute } from "./ingress";
export { IngressRouteTcp } from "./ingress-tcp";

View File

@@ -0,0 +1,71 @@
import { Construct } from "constructs";
import { Manifest } from "@cdktf/provider-kubernetes/lib/manifest";
import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
export interface IngressRouteTcpOptions {
provider: KubernetesProvider;
/** Namespace where the IngressRouteTCP will be created */
namespace: string;
/** EntryPoint name (e.g., "ssh", "mc25565", "postgres", etc.) */
entryPoint: string;
/** Backend service name */
serviceName: string;
/** Backend service port */
servicePort: number;
/**
* Match rule.
* Default is `HostSNI(\`*\`)` which is correct for most TCP services.
*/
match?: string;
/** Name override (CR name) */
name?: string;
}
export class IngressRouteTcp extends Construct {
public readonly manifest: Manifest;
constructor(scope: Construct, id: string, opts: IngressRouteTcpOptions) {
super(scope, id);
const name =
opts.name ??
`tcp-${opts.entryPoint}-${opts.serviceName}`.replace(
/[^a-zA-Z0-9-]/g,
"",
);
const matchRule = opts.match ?? "HostSNI(`*`)";
this.manifest = new Manifest(this, name, {
provider: opts.provider,
manifest: {
apiVersion: "traefik.io/v1alpha1",
kind: "IngressRouteTCP",
metadata: {
name,
namespace: opts.namespace,
},
spec: {
entryPoints: [opts.entryPoint],
routes: [
{
match: matchRule,
services: [
{
name: opts.serviceName,
port: opts.servicePort,
},
],
},
],
},
},
});
}
}

View File

@@ -4,7 +4,7 @@ import { KubernetesProvider } from "@cdktf/provider-kubernetes/lib/provider";
import { CloudflareCertificate } from "../cert-manager"; import { CloudflareCertificate } from "../cert-manager";
export interface TraefikIngressRouteOptions { export interface IngressRouteOptions {
provider: KubernetesProvider; provider: KubernetesProvider;
namespace: string; namespace: string;
@@ -31,10 +31,10 @@ export interface TraefikIngressRouteOptions {
name?: string; name?: string;
} }
export class TraefikIngressRoute extends Construct { export class IngressRoute extends Construct {
public readonly manifest: Manifest; public readonly manifest: Manifest;
constructor(scope: Construct, id: string, opts: TraefikIngressRouteOptions) { constructor(scope: Construct, id: string, opts: IngressRouteOptions) {
super(scope, id); super(scope, id);
const name = opts.name ?? `route-${opts.host.replace(/\./g, "-")}`; const name = opts.name ?? `route-${opts.host.replace(/\./g, "-")}`;