| name | cdk8s-abstractions |
| description | Use when asking about ZFS volumes, TailscaleIngress, Tailscale ingress, Funnel public access, container props, Redis construct, or other reusable CDK8s abstractions. |
CDK8s Abstractions
Overview
This project provides reusable CDK8s constructs for common patterns: storage volumes with backup
labeling, Tailscale networking, container property composition, and database services.
Storage Constructs
ZfsNvmeVolume
Creates a PVC on NVMe storage with automatic Velero backup labeling.
import { ZfsNvmeVolume } from "../misc/zfs-nvme-volume.ts";
import { Size } from "cdk8s";
const configVolume = new ZfsNvmeVolume(chart, "myapp-config-pvc", {
storage: Size.gibibytes(8),
});
volumeMounts: [
{
path: "/config",
volume: Volume.fromPersistentVolumeClaim(
chart,
"myapp-config-volume",
configVolume.claim,
),
},
],
ZfsSataVolume
Creates a PVC on SATA SSD storage for large data (uses K8s storage class zfs-hdd for legacy reasons).
import { ZfsSataVolume } from "../misc/zfs-sata-volume.ts";
import { Size } from "cdk8s";
const mediaVolume = new ZfsSataVolume(chart, "media-hdd-pvc", {
storage: Size.tebibytes(4),
});
Automatic Backup Labeling
Both constructs auto-label PVCs for Velero:
| Volume Size | Backup Status |
|---|
| < 200 GB | velero.io/backup: enabled |
| >= 200 GB | velero.io/backup: disabled |
const shouldBackup = props.storage.toKibibytes() < Size.gibibytes(200).toKibibytes();
metadata: {
labels: {
"velero.io/backup": shouldBackup ? "enabled" : "disabled",
"velero.io/exclude-from-backup": shouldBackup ? "false" : "true",
},
},
Networking Constructs
TailscaleIngress
Creates an Ingress with the Tailscale ingress class. Services are exposed via the Tailscale Kubernetes operator for secure private network access.
import { TailscaleIngress } from "../misc/tailscale.ts";
import { Service } from "cdk8s-plus-31";
const service = new Service(chart, "myapp-service", {
selector: deployment,
ports: [{ port: 8080 }],
});
new TailscaleIngress(chart, "myapp-ingress", {
service,
host: "myapp",
});
new TailscaleIngress(chart, "myapp-ingress", {
service,
host: "myapp",
funnel: true,
});
DNS pattern: All services follow {host}.tailnet-1a49.ts.net (e.g., sonarr.tailnet-1a49.ts.net, argocd.tailnet-1a49.ts.net).
Implementation Details
export class TailscaleIngress extends Construct {
constructor(
scope: Construct,
id: string,
props: Partial<IngressProps> & {
host: string;
funnel?: boolean;
service: Service | ServiceObject;
},
) {
super(scope, id);
let base: IngressProps = {
defaultBackend: IngressBackend.fromService(props.service as Service),
tls: [{ hosts: [props.host] }],
};
if (props.funnel) {
base = {
...base,
metadata: {
annotations: {
"tailscale.com/funnel": "true",
},
},
};
}
const ingress = new Ingress(scope, `${id}-ingress`, merge({}, base, props));
ApiObject.of(ingress).addJsonPatch(
JsonPatch.add("/spec/ingressClassName", "tailscale"),
);
}
}
TLS is automatically handled by Tailscale — no secretName needed in the tls config.
createIngress Function
Alternative function-based API for ArgoCD applications or when you need more control:
import { createIngress } from "../misc/tailscale.ts";
createIngress(
chart,
"myapp-ingress",
"myapp",
"myapp-service",
8080,
["myapp"],
false,
);
Signature:
export function createIngress(
chart: Chart,
name: string,
namespace: string,
service: string,
port: number,
hosts: string[],
funnel: boolean,
): KubeIngress;
Tailscale Ingress Patterns
ArgoCD application ingress:
createIngress(
chart,
"argocd-ingress",
"argocd",
"argocd-server",
443,
["argocd"],
true,
);
Multiple ingresses for same deployment:
new TailscaleIngress(chart, "myapp-web-ingress", {
service: webService,
host: "myapp",
});
new TailscaleIngress(chart, "myapp-metrics-ingress", {
service: metricsService,
host: "myapp-metrics",
});
External service reference:
new TailscaleIngress(chart, "external-ingress", {
service: { name: "external-service", port: 443 } as unknown as Service,
host: "external",
});
When to Use Funnel
Use Funnel for: External webhook receivers, public-facing web apps, GitHub Actions integrations, OAuth callbacks.
Do NOT use Funnel for: Internal tools (Sonarr, Radarr), admin dashboards (ArgoCD, Grafana), database connections, sensitive services.
Tailscale Operator Configuration
const tailscaleValues: HelmValuesForChart<"tailscale-operator"> = {
oauth: { clientId: "...", clientSecret: "..." },
operatorConfig: { hostname: "homelab-operator" },
};
Debugging Tailscale Ingress
kubectl get ingress -A
kubectl describe ingress myapp-ingress -n media
kubectl logs -n tailscale deployment/tailscale-operator
tailscale status
nslookup myapp.tailnet-1a49.ts.net
Container Property Composition
withCommonProps
Applies base properties to all containers:
import { withCommonProps } from "../misc/common.ts";
deployment.addContainer(
withCommonProps({
image: "myimage:latest",
portNumber: 8080,
}),
);
What it adds:
TZ: America/Los_Angeles environment variable
- Empty resources object (ready for limits)
withCommonLinuxServerProps
Extends withCommonProps for linuxserver.io images:
import { withCommonLinuxServerProps, LINUXSERVER_GID } from "../misc/linux-server.ts";
const deployment = new Deployment(chart, "myapp", {
replicas: 1,
strategy: DeploymentStrategy.recreate(),
securityContext: {
fsGroup: LINUXSERVER_GID,
},
});
deployment.addContainer(
withCommonLinuxServerProps({
image: `ghcr.io/linuxserver/myapp:${versions["linuxserver/myapp"]}`,
portNumber: 8080,
volumeMounts: [...],
}),
);
What it adds:
PUID: 1000 and PGID: 1000 environment variables
TZ: America/Los_Angeles
- Security context allowing root (for permission fixing)
Database Constructs
Redis
Deploys Redis via ArgoCD with Bitnami Helm chart:
import { Redis } from "../resources/common/redis.ts";
const redis = new Redis(chart, "myapp-redis", {
namespace: "media",
});
envVariables: {
REDIS_HOST: EnvValue.fromValue(redis.serviceName),
REDIS_PORT: EnvValue.fromValue("6379"),
},
Features:
- Standalone architecture (no replication)
- No authentication (internal use)
- No persistence (in-memory only)
- Auto-creates ArgoCD Application
PostalMariaDB
Specialized MariaDB for Postal mail server:
import { PostalMariaDB } from "../resources/postgres/postal-mariadb.ts";
const mariadb = new PostalMariaDB(chart, "postal-mariadb", {
namespace: "postal",
storageClass: "zfs-ssd",
storageSize: "32Gi",
});
mariadb.serviceName;
mariadb.databaseName;
mariadb.username;
mariadb.secretItem;
Utility Patterns
JsonPatch for Unsupported Fields
Add fields CDK8s doesn't expose:
import { ApiObject, JsonPatch } from "cdk8s";
ApiObject.of(deployment).addJsonPatch(
JsonPatch.add("/spec/template/spec/containers/0/resources", {
limits: {
"gpu.intel.com/i915": 1,
},
}),
);
ApiObject.of(deployment).addJsonPatch(
JsonPatch.add("/spec/template/spec/hostNetwork", true),
);
OnePasswordItem for Secrets
import { OnePasswordItem } from "../generated/imports/onepassword.com.ts";
const secrets = new OnePasswordItem(chart, "myapp-secrets", {
spec: {
itemPath: "vaults/xxx/items/yyy",
},
});
envVariables: {
API_KEY: EnvValue.fromSecretValue({
secret: Secret.fromSecretName(chart, "api-key-ref", secrets.name),
key: "password",
}),
},
ConfigMap from Directory
import { ConfigMap, Volume } from "cdk8s-plus-31";
const config = new ConfigMap(chart, "myapp-config");
config.addDirectory(`${import.meta.dir}/../../../config/myapp`);
const configVolume = Volume.fromConfigMap(chart, "myapp-config-volume", config);
volumeMounts: files.map((file) => ({
path: `/config/${file}`,
subPath: file,
volume: configVolume,
})),
Constants
export const NVME_STORAGE_CLASS = "zfs-ssd";
export const SATA_STORAGE_CLASS = "zfs-hdd";
export const ROOT_UID = 0;
export const ROOT_GID = 0;
export const LINUXSERVER_UID = 1000;
export const LINUXSERVER_GID = 1000;
Key Files
src/cdk8s/src/misc/zfs-nvme-volume.ts - SSD volume construct
src/cdk8s/src/misc/zfs-sata-volume.ts - HDD volume construct
src/cdk8s/src/misc/tailscale.ts - Tailscale ingress construct
src/cdk8s/src/misc/common.ts - Base container props
src/cdk8s/src/misc/linux-server.ts - LinuxServer.io props
src/cdk8s/src/misc/storage-classes.ts - Storage class constants
src/cdk8s/src/resources/common/redis.ts - Redis construct
src/cdk8s/src/resources/postgres/postal-mariadb.ts - MariaDB construct