Claude
Skills
Sign in
Back

cdk8s-abstractions

Included with Lifetime
$97 forever

Use when asking about ZFS volumes, TailscaleIngress, Tailscale ingress, Funnel public access, container props, Redis construct, or other reusable CDK8s abstractions.

Ads & Marketing

What this skill does


# 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.

```typescript
import { ZfsNvmeVolume } from "../misc/zfs-nvme-volume.ts";
import { Size } from "cdk8s";

const configVolume = new ZfsNvmeVolume(chart, "myapp-config-pvc", {
  storage: Size.gibibytes(8),
});

// Use in deployment
volumeMounts: [
  {
    path: "/config",
    volume: Volume.fromPersistentVolumeClaim(
      chart,
      "myapp-config-volume",
      configVolume.claim,  // Access the PVC
    ),
  },
],
```

### ZfsSataVolume

Creates a PVC on SATA SSD storage for large data (uses K8s storage class `zfs-hdd` for legacy reasons).

```typescript
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` |

```typescript
// Implementation detail
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.

```typescript
import { TailscaleIngress } from "../misc/tailscale.ts";
import { Service } from "cdk8s-plus-31";

const service = new Service(chart, "myapp-service", {
  selector: deployment,
  ports: [{ port: 8080 }],
});

// Basic usage (private Tailscale access)
new TailscaleIngress(chart, "myapp-ingress", {
  service,
  host: "myapp", // Accessible at myapp.tailnet-1a49.ts.net
});

// Public internet access via Funnel
new TailscaleIngress(chart, "myapp-ingress", {
  service,
  host: "myapp",
  funnel: true, // Accessible from public internet
});
```

**DNS pattern:** All services follow `{host}.tailnet-1a49.ts.net` (e.g., `sonarr.tailnet-1a49.ts.net`, `argocd.tailnet-1a49.ts.net`).

#### Implementation Details

```typescript
// From src/cdk8s/src/misc/tailscale.ts
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));

    // Set ingress class to Tailscale
    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:

```typescript
import { createIngress } from "../misc/tailscale.ts";

createIngress(
  chart,
  "myapp-ingress", // Ingress name
  "myapp", // Namespace
  "myapp-service", // Service name (string, not object)
  8080, // Port
  ["myapp"], // Hosts
  false, // Funnel enabled
);
```

**Signature:**

```typescript
export function createIngress(
  chart: Chart,
  name: string,
  namespace: string,
  service: string,
  port: number,
  hosts: string[],
  funnel: boolean,
): KubeIngress;
```

### Tailscale Ingress Patterns

**ArgoCD application ingress:**

```typescript
createIngress(
  chart,
  "argocd-ingress",
  "argocd",
  "argocd-server",
  443,
  ["argocd"],
  true,
);
```

**Multiple ingresses for same deployment:**

```typescript
new TailscaleIngress(chart, "myapp-web-ingress", {
  service: webService,
  host: "myapp",
});
new TailscaleIngress(chart, "myapp-metrics-ingress", {
  service: metricsService,
  host: "myapp-metrics",
});
```

**External service reference:**

```typescript
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

```typescript
// From src/cdk8s/src/resources/argo-applications/tailscale.ts
const tailscaleValues: HelmValuesForChart<"tailscale-operator"> = {
  oauth: { clientId: "...", clientSecret: "..." },
  operatorConfig: { hostname: "homelab-operator" },
};
```

### Debugging Tailscale Ingress

```bash
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:

```typescript
import { withCommonProps } from "../misc/common.ts";

deployment.addContainer(
  withCommonProps({
    image: "myimage:latest",
    portNumber: 8080,
    // ... other props
  }),
);
```

**What it adds:**

- `TZ: America/Los_Angeles` environment variable
- Empty resources object (ready for limits)

### withCommonLinuxServerProps

Extends `withCommonProps` for linuxserver.io images:

```typescript
import { withCommonLinuxServerProps, LINUXSERVER_GID } from "../misc/linux-server.ts";

const deployment = new Deployment(chart, "myapp", {
  replicas: 1,
  strategy: DeploymentStrategy.recreate(),
  securityContext: {
    fsGroup: LINUXSERVER_GID,  // Important!
  },
});

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:

```typescript
import { Redis } from "../resources/common/redis.ts";

const redis = new Redis(chart, "myapp-redis", {
  namespace: "media",  // Use the appropriate service namespace
});

// Use in deployment
envVariables: {
  REDIS_HOST: EnvValue.fromValue(redis.serviceName),  // "myapp-redis-master"
  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:

```typescript
import { PostalMariaDB } from "../resources/postgres/postal-mariadb.ts";

const mariadb = new PostalMariaDB(chart, "postal-mariadb", {
  namespace: "postal",
  storageClass: "zfs-ssd",
  storageSize: "32Gi",
});

// Access properties
mariadb.serviceName; // "postal-mariadb"
mariadb.databaseName; // "postal"
mariadb.username; // "postal"
mariadb.secretItem; // OnePasswordItem for credentials
```

## Utility Patterns

### JsonPatch for Unsupported Fields

Add fields CDK8s doesn't expose:

```typescript
import { ApiObject, JsonPatch } from "cdk8s";

// Add GPU resource li

Related in Ads & Marketing