Skip to content

Part 2: Namespaces & Configuration

In Part 1 you deployed podinfo into the default namespace. Now you’ll give the app a namespace of its own, declared as a raw Kubernetes object. Then you’ll configure the app from your program.

Anything that isn’t a Deployment or a Job is a Kubernetes.Manifest. A Manifest is a literal Kubernetes object, written exactly as you’d write it in YAML. Add a Namespace:

Effect.gen(function* () {
const namespace = yield* Kubernetes.Manifest("Namespace", {
cluster,
manifest: {
apiVersion: "v1",
kind: "Namespace",
metadata: { name: "my-app" },
},
});
const web = yield* Kubernetes.Deployment("Web", {

Alchemy applies the object with server-side apply and tracks it like any other resource. Any kind works, such as ConfigMap, Secret, StatefulSet, Ingress, or a custom resource from a CRD.

Point the Deployment at the new namespace:

const web = yield* Kubernetes.Deployment("Web", {
cluster,
name: "web",
namespace: namespace.name,
image: "ghcr.io/stefanprodan/podinfo:6.15.0",
port: 9898,
replicas: 2,
serviceType: "ClusterIP",
});

namespace.name is an Output. It’s a reference to the Manifest’s attribute that resolves at deploy time. Passing it instead of the string "my-app" makes the Deployment depend on the Namespace, so Alchemy creates the Namespace first. Workload resources expect their namespace to exist already.

podinfo shows the value of PODINFO_UI_MESSAGE in its response. Set it with env:

const web = yield* Kubernetes.Deployment("Web", {
cluster,
name: "web",
namespace: namespace.name,
image: "ghcr.io/stefanprodan/podinfo:6.15.0",
port: 9898,
replicas: 2,
serviceType: "ClusterIP",
env: {
PODINFO_UI_MESSAGE: "hello from alchemy",
},
});

Every key in env becomes a container environment variable. Values can also be Outputs from other resources, such as a database URL or a bucket name. Alchemy resolves them before applying.

Tell the scheduler what each pod needs:

env: {
PODINFO_UI_MESSAGE: "hello from alchemy",
},
resources: {
requests: { cpu: "10m", memory: "32Mi" },
limits: { memory: "64Mi" },
},
});

resources maps directly onto the container’s Kubernetes resources field and takes the usual Kubernetes quantities.

Terminal window
bun alchemy deploy
Plan: 1 to create, 1 to update

+ Namespace (Kubernetes.Manifest)
~ Web (Kubernetes.Deployment)

Proceed?
◉ Yes ○ No
✓ Namespace (Kubernetes.Manifest) created
✓ Web (Kubernetes.Deployment) updated
{
  context: "kind-alchemy",
  namespace: "my-app",
  service: "web",
}

The Namespace is created first. The Deployment update applies its objects into my-app and deletes the old ones from default. Alchemy records every object it applies, and anything that drops out of the desired set is pruned.

The default namespace is empty again, and the app runs in my-app:

Terminal window
kubectl --context kind-alchemy get deployments --all-namespaces | grep web
kubectl --context kind-alchemy -n my-app rollout status deployment/web
kubectl --context kind-alchemy -n my-app port-forward service/web 9898:9898
Terminal window
curl -s localhost:9898 | grep message
"message": "hello from alchemy",

Edit the message and deploy:

env: {
PODINFO_UI_MESSAGE: "hello from alchemy",
PODINFO_UI_MESSAGE: "configured in TypeScript",
},

The plan shows one update. Alchemy re-applies the Deployment and Kubernetes rolls the pods over to the new environment, one replica at a time.

You now have:

  • A my-app Namespace declared as a raw manifest
  • The Deployment moved into it, ordered after the Namespace by an Output reference
  • Environment variables and resource limits set from your program

In Part 3, you’ll run one-off and scheduled work with Kubernetes.Job.