Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions examples/kubernetes-local/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Kubernetes (local cluster) Example

The end state of the [Kubernetes tutorial](https://alchemy.run/kubernetes/tutorial/part-1)
on any cluster your kubeconfig can reach — Docker Desktop, OrbStack, kind, k3s — with
no cloud account and no container registry:

- a `tutorial` Namespace, an `app-config` ConfigMap, and a Redis StatefulSet + Service,
all as `Kubernetes.Manifest`
- an echo server as a `Kubernetes.Deployment` (pre-built `image:`), wired to the
ConfigMap and Redis through `env`
- a one-shot `Kubernetes.Job` and a `Kubernetes.Job` with `schedule` (a CronJob)
- the `podinfo` Helm chart as a `Kubernetes.HelmChart`, rendered with the local
`helm` CLI and applied as objects

## Commands

```sh
bun install
bun run --filter kubernetes-local-example deploy
bun run --filter kubernetes-local-example destroy
```

`alchemy.run.ts` uses your kubeconfig's current context; pass
`Kubernetes.KubeConfig({ context: "docker-desktop" })` to pin one. `helm` must be
installed for the chart.

## Try it

```sh
# Docker Desktop / OrbStack publish the LoadBalancer on localhost:
curl "$url/hello"

# kind / minikube have no LoadBalancer controller — port-forward instead:
kubectl -n tutorial port-forward "svc/$deployment" 8080:8080 &
curl localhost:8080/hello
```
146 changes: 146 additions & 0 deletions examples/kubernetes-local/alchemy.run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import * as Alchemy from "alchemy";
import * as Kubernetes from "alchemy/Kubernetes";
import * as Output from "alchemy/Output";
import * as Effect from "effect/Effect";

// Settings shared between the ConfigMap and the Deployment's env.
const settings = {
LOG_LEVEL: "debug",
GREETING: "hello from alchemy",
};

// The end state of the Kubernetes tutorial (parts 1–4), on whichever
// cluster your kubeconfig's current context points at: a Namespace,
// a ConfigMap, a Redis StatefulSet + Service, an echo Deployment, a
// one-shot Job, a CronJob, and a Helm chart — all pre-built images,
// so no registry is needed.
export default Alchemy.Stack(
"KubernetesLocalExample",
{
providers: Kubernetes.providers(),
state: Alchemy.localState(),
},
Effect.gen(function* () {
// Any cluster `kubectl` can reach. Pass `{ context: "..." }` to pin one.
const cluster = Kubernetes.KubeConfig();

const ns = yield* Kubernetes.Manifest("Namespace", {
cluster,
manifest: {
apiVersion: "v1",
kind: "Namespace",
metadata: { name: "tutorial" },
},
});

const config = yield* Kubernetes.Manifest("Config", {
cluster,
manifest: {
apiVersion: "v1",
kind: "ConfigMap",
metadata: { name: "app-config", namespace: ns.name },
data: settings,
},
});

const redis = yield* Kubernetes.Manifest("Redis", {
cluster,
manifest: {
apiVersion: "apps/v1",
kind: "StatefulSet",
metadata: { name: "redis", namespace: ns.name },
spec: {
serviceName: "redis",
replicas: 1,
selector: { matchLabels: { app: "redis" } },
template: {
metadata: { labels: { app: "redis" } },
spec: {
containers: [
{
name: "redis",
image: "redis:7",
ports: [{ containerPort: 6379 }],
},
],
},
},
},
},
});

const redisService = yield* Kubernetes.Manifest("RedisService", {
cluster,
manifest: {
apiVersion: "v1",
kind: "Service",
metadata: { name: "redis", namespace: ns.name },
spec: {
type: "ClusterIP",
selector: { app: "redis" },
ports: [{ port: 6379, targetPort: 6379 }],
},
},
});

// A replicated server from a pre-built image. `LoadBalancer` (the
// default) populates `url` on clusters with an LB implementation
// (Docker Desktop, OrbStack); kind/minikube leave it undefined —
// use NodePort or `kubectl port-forward` there.
const echo = yield* Kubernetes.Deployment("Echo", {
cluster,
namespace: ns.name,
image: "mendhak/http-https-echo:33",
port: 8080,
replicas: 3,
env: {
...settings,
CONFIG_MAP: config.name,
REDIS_URL: Output.interpolate`redis://${redisService.name}:6379`,
},
});

// Run-to-completion work.
const hello = yield* Kubernetes.Job("Hello", {
cluster,
namespace: ns.name,
image: "busybox:1.36",
command: ["sh", "-c"],
args: ["echo hello from a Job && sleep 2"],
backoffLimit: 1,
ttlSecondsAfterFinished: 300,
});

// ...and the same thing on a schedule (a CronJob).
const tick = yield* Kubernetes.Job("Tick", {
cluster,
namespace: ns.name,
image: "busybox:1.36",
command: ["sh", "-c", "date"],
schedule: "*/5 * * * *",
});

// A third-party Helm chart, rendered locally and applied as objects.
const podinfo = yield* Kubernetes.HelmChart("Podinfo", {
cluster,
chart: "podinfo",
repo: "https://stefanprodan.github.io/podinfo",
version: "6.14.1",
namespace: "podinfo",
createNamespace: true,
values: {
replicaCount: 2,
ui: { message: "hello from alchemy" },
},
});

return {
url: echo.url,
deployment: echo.deploymentName,
redis: redis.name,
job: hello.jobName,
cron: tick.jobName,
release: podinfo.releaseName,
};
}),
);
21 changes: 21 additions & 0 deletions examples/kubernetes-local/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "kubernetes-local-example",
"version": "0.0.0",
"private": true,
"license": "Apache-2.0",
"repository": {
"type": "git",
"url": "git+https://github.com/alchemy-run/alchemy.git",
"directory": "examples/kubernetes-local"
},
"scripts": {
"build": "tsc -b",
"deploy": "alchemy deploy",
"destroy": "alchemy destroy"
},
"dependencies": {
"@effect/platform-node": "catalog:effect",
"alchemy": "workspace:*",
"effect": "catalog:effect"
}
}
16 changes: 16 additions & 0 deletions examples/kubernetes-local/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.base.json",
"include": ["alchemy.run.ts", "src/**/*.ts"],
"compilerOptions": {
"noEmit": true,
"rootDir": ".",
"module": "Preserve",
"moduleResolution": "Bundler",
"target": "ESNext"
},
"references": [
{
"path": "../../packages/alchemy/tsconfig.json"
}
]
}
4 changes: 3 additions & 1 deletion packages/alchemy/src/Kubernetes/HelmChart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,9 @@ const resolveReleaseName = (
Effect.suspend(() => {
if (news.releaseName) return Effect.succeed(news.releaseName);
if (output?.releaseName) return Effect.succeed(output.releaseName);
return createPhysicalName({ id, lowercase: true });
// Helm caps release names at 53 characters (they prefix every object
// name the chart renders, which must stay a valid DNS label).
return createPhysicalName({ id, lowercase: true, maxLength: 53 });
});

/**
Expand Down
6 changes: 5 additions & 1 deletion packages/alchemy/src/Kubernetes/internal/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,11 @@ export const deleteObject = Effect.fn(function* ({
requestJson({
transport,
method: "DELETE",
path,
// The REST default propagation policy is per-kind — batch/v1 Jobs
// (and CronJobs) default to `Orphan`, which leaves their pods
// behind on delete. Ask for background cascading deletion the way
// `kubectl delete` does so dependents are garbage-collected.
path: `${path}?propagationPolicy=Background`,
}),
),
Effect.catchIf(
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@
{
"path": "./examples/aws-eks/tsconfig.json"
},
{
"path": "./examples/kubernetes-local/tsconfig.json"
},
{
"path": "./examples/aws-bedrock-ai/tsconfig.json"
},
Expand Down
67 changes: 67 additions & 0 deletions website/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function providersSidebarEntry() {
{ label: "Cloudflare", link: "/cloudflare" },
{ label: "Hetzner", link: "/hetzner" },
{ label: "Fly", link: "/fly" },
{ label: "Kubernetes", link: "/kubernetes" },
{ label: "PlanetScale", link: "/planetscale" },
{ label: "Neon", link: "/neon" },
{ label: "Prisma", link: "/prisma" },
Expand Down Expand Up @@ -1022,6 +1023,72 @@ export default defineConfig({
providerResourcesEntry("Fly"),
],
},
{
label: "Kubernetes",
items: [
{ label: "Overview", link: "/kubernetes" },
{ label: "Setup", link: "/kubernetes/setup" },
{
label: "Tutorial",
items: [{ autogenerate: { directory: "kubernetes/tutorial" } }],
},
{
label: "Clusters",
items: [
{
label: "Connecting",
link: "/kubernetes/clusters/connecting",
},
{ label: "Local clusters", link: "/kubernetes/clusters/local" },
{ label: "Amazon EKS", link: "/kubernetes/clusters/eks" },
],
},
{
label: "Workloads",
items: [
{
label: "Deployments",
link: "/kubernetes/workloads/deployments",
},
{ label: "Jobs", link: "/kubernetes/workloads/jobs" },
{
label: "Image sources",
link: "/kubernetes/workloads/image-sources",
},
{ label: "Bindings", link: "/kubernetes/workloads/bindings" },
{
label: "Pod template",
link: "/kubernetes/workloads/pod-template",
},
],
},
{
label: "Objects",
items: [
{ label: "Manifests", link: "/kubernetes/objects/manifests" },
{ label: "Helm charts", link: "/kubernetes/objects/helm" },
],
},
{
label: "Guides",
items: [
{
label: "Exposing services",
link: "/kubernetes/guides/exposing-services",
},
{
label: "How apply works",
link: "/kubernetes/guides/how-apply-works",
},
{
label: "Cluster adapters",
link: "/kubernetes/guides/cluster-adapters",
},
],
},
providerResourcesEntry("Kubernetes"),
],
},
{
label: "PlanetScale",
items: [
Expand Down
4 changes: 4 additions & 0 deletions website/src/components/ProviderDirectory.astro
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,10 @@ const HUBS: Record<string, { href: string; blurb: string }> = {
href: "/fly",
blurb: "Machines, Services, Volumes, Secrets, and fly.dev on Fly.io",
},
Kubernetes: {
href: "/kubernetes",
blurb: "Deployments, Jobs, Manifests, and Helm charts on any cluster",
},
Planetscale: {
href: "/planetscale",
blurb: "Serverless MySQL & Postgres with branching workflows",
Expand Down
13 changes: 9 additions & 4 deletions website/src/content/docs/aws/compute/choosing-a-runtime.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,21 @@ load-balancer integration. Alchemy models it with:

- [`Cluster`](/providers/aws/eks/cluster) — the control plane;
`compute: "auto"` turns on Auto Mode with sensible defaults.
- [`Deployment`](/providers/kubernetes/deployment) — a replicated
- [`Deployment`](/kubernetes/workloads/deployments) — a replicated
Kubernetes server (the Kubernetes analog of `AWS.ECS.Service`)
with the same three image sources as ECS: a registry `image`, a
Dockerfile `context`, or an inline Effect program via `main`.
- [`Job`](/providers/kubernetes/job) — run-to-completion work, or a
- [`Job`](/kubernetes/workloads/jobs) — run-to-completion work, or a
`CronJob` when you set `schedule`.
- [`Manifest`](/providers/kubernetes/manifest) and
[`HelmChart`](/providers/kubernetes/helmchart) — any raw Kubernetes
- [`Manifest`](/kubernetes/objects/manifests) and
[`HelmChart`](/kubernetes/objects/helm) — any raw Kubernetes
object or a rendered Helm chart, applied via server-side apply.

The workloads are cluster-agnostic — the same program runs on any
cluster your kubeconfig can reach; see the
[Kubernetes hub](/kubernetes) for the workloads themselves and
[Amazon EKS](/kubernetes/clusters/eks) for what the EKS adapter adds.

Bindings work the same as on Lambda and ECS — env vars plus IAM
policy statements, delivered through an EKS Pod Identity role —
and there is no YAML and no `kubectl` step. You pay for the
Expand Down
Loading
Loading