| 1 | --- |
| 2 | title: Local clusters |
| 3 | description: Run a Kubernetes cluster on your machine with Kubernetes.LocalCluster. It is a kind cluster in Docker with an image registry wired into its nodes, so Effect programs and Dockerfiles deploy the same way they do on a hosted cluster. |
| 4 | --- |
| 5 | |
| 6 | [`Kubernetes.LocalCluster`](/providers/kubernetes/localcluster) runs |
| 7 | a Kubernetes cluster on your machine. Use it to try Alchemy's |
| 8 | Kubernetes resources, develop against a real cluster without paying |
| 9 | for one, or run tests. |
| 10 | |
| 11 | ```typescript |
| 12 | const cluster = yield* Kubernetes.LocalCluster("Cluster", { |
| 13 | name: "alchemy", |
| 14 | }); |
| 15 | |
| 16 | const web = yield* Kubernetes.Deployment("Web", { |
| 17 | cluster, |
| 18 | image: "ghcr.io/stefanprodan/podinfo:6.15.0", |
| 19 | port: 9898, |
| 20 | serviceType: "ClusterIP", |
| 21 | }); |
| 22 | ``` |
| 23 | |
| 24 | Unlike [`Kubernetes.KubeConfig`](/kubernetes/clusters/connecting), |
| 25 | which describes a cluster that already exists, `LocalCluster` is a |
| 26 | resource. Deploying creates the cluster, and destroying deletes it. |
| 27 | |
| 28 | ## What it creates |
| 29 | |
| 30 | A `LocalCluster` is two Docker containers: |
| 31 | |
| 32 | - a single-node [kind](https://kind.sigs.k8s.io) cluster named |
| 33 | `name`, and |
| 34 | - a `registry:3` image registry named `<name>-registry`, |
| 35 | published on `localhost:<registryPort>`. |
| 36 | |
| 37 | Alchemy connects the registry to the cluster's Docker network and |
| 38 | configures the node to pull `localhost:<registryPort>/…` images from |
| 39 | it, following kind's |
| 40 | [local registry](https://kind.sigs.k8s.io/docs/user/local-registry/) |
| 41 | setup. Creating the cluster takes about 30 seconds. |
| 42 | |
| 43 | ## Requirements |
| 44 | |
| 45 | - [Docker](https://docs.docker.com/get-started/get-docker/) must be |
| 46 | running. |
| 47 | - [kind](https://kind.sigs.k8s.io/docs/user/quick-start/#installation) |
| 48 | must be installed. On macOS, run `brew install kind`. Set `KIND_BIN` |
| 49 | to use a binary that isn't on `PATH`. |
| 50 | |
| 51 | ## Pass it as the cluster |
| 52 | |
| 53 | The resource's `connection` attribute carries everything a workload |
| 54 | needs, so pass the whole resource as `cluster`: |
| 55 | |
| 56 | - **Authentication.** It uses the kubeconfig context `kind-<name>`. |
| 57 | - **Registry.** It points at `localhost:<registryPort>`, so `main` |
| 58 | programs and `context` Dockerfiles are built and pushed there. |
| 59 | - **Architecture.** It matches your Docker host's CPU architecture, |
| 60 | so images are built for the node without setting `architecture`. |
| 61 | |
| 62 | Effect programs therefore run without extra setup: |
| 63 | |
| 64 | ```typescript |
| 65 | const hello = yield* Kubernetes.Job( |
| 66 | "Hello", |
| 67 | { cluster, main: import.meta.url }, |
| 68 | Effect.gen(function* () { |
| 69 | return { |
| 70 | run: Effect.log("hello from a Job"), |
| 71 | }; |
| 72 | }), |
| 73 | ); |
| 74 | ``` |
| 75 | |
| 76 | ## Use kubectl |
| 77 | |
| 78 | kind adds the context `kind-<name>` to your kubeconfig, so `kubectl` |
| 79 | works against the cluster directly: |
| 80 | |
| 81 | ```sh |
| 82 | kubectl --context kind-alchemy get pods --all-namespaces |
| 83 | ``` |
| 84 | |
| 85 | Set `kubeconfig` to write the context to a different file instead |
| 86 | of `$KUBECONFIG` or `~/.kube/config`: |
| 87 | |
| 88 | ```typescript |
| 89 | const cluster = yield* Kubernetes.LocalCluster("Cluster", { |
| 90 | name: "alchemy", |
| 91 | kubeconfig: "./.alchemy/kubeconfig", |
| 92 | }); |
| 93 | ``` |
| 94 | |
| 95 | ## Reach services |
| 96 | |
| 97 | A kind cluster has no cloud load balancer. Use |
| 98 | `serviceType: "ClusterIP"` or `"NodePort"` for Deployments, and |
| 99 | reach them with `kubectl port-forward`: |
| 100 | |
| 101 | ```sh |
| 102 | kubectl --context kind-alchemy port-forward service/web 9898:9898 |
| 103 | ``` |
| 104 | |
| 105 | A `LoadBalancer` Service would wait about three minutes for an |
| 106 | address that never arrives. |
| 107 | |
| 108 | ## Expose services on localhost |
| 109 | |
| 110 | To reach a service without `port-forward`, forward a host port to the |
| 111 | node. This maps `localhost:8080` to port 30080 on the node: |
| 112 | |
| 113 | ```typescript |
| 114 | const cluster = yield* Kubernetes.LocalCluster("Cluster", { |
| 115 | name: "alchemy", |
| 116 | nodes: [ |
| 117 | { |
| 118 | role: "control-plane", |
| 119 | extraPortMappings: [{ containerPort: 30080, hostPort: 8080 }], |
| 120 | }, |
| 121 | ], |
| 122 | }); |
| 123 | ``` |
| 124 | |
| 125 | Then give the service a `NodePort` Service on that port. A Deployment |
| 126 | labels its pods `app.kubernetes.io/name: <name>`, so a |
| 127 | [Manifest](/kubernetes/objects/manifests) can select them: |
| 128 | |
| 129 | ```typescript |
| 130 | const web = yield* Kubernetes.Deployment("Web", { |
| 131 | cluster, |
| 132 | name: "web", |
| 133 | image: "ghcr.io/stefanprodan/podinfo:6.15.0", |
| 134 | port: 9898, |
| 135 | serviceType: "ClusterIP", |
| 136 | }); |
| 137 | |
| 138 | yield* Kubernetes.Manifest("WebNodePort", { |
| 139 | cluster, |
| 140 | manifest: { |
| 141 | apiVersion: "v1", |
| 142 | kind: "Service", |
| 143 | metadata: { name: "web-nodeport", namespace: web.namespace }, |
| 144 | spec: { |
| 145 | type: "NodePort", |
| 146 | selector: { "app.kubernetes.io/name": web.deploymentName }, |
| 147 | ports: [{ port: 9898, targetPort: 9898, nodePort: 30080 }], |
| 148 | }, |
| 149 | }, |
| 150 | }); |
| 151 | ``` |
| 152 | |
| 153 | `curl localhost:8080` now reaches podinfo. The same approach puts an |
| 154 | ingress controller or Gateway implementation on ports 80 and 443: |
| 155 | map those ports on the node, then install the controller with a |
| 156 | [Helm chart](/kubernetes/objects/helm-charts), configured for kind. |
| 157 | |
| 158 | ## Configure the cluster |
| 159 | |
| 160 | `LocalCluster` accepts the fields of a |
| 161 | [kind cluster configuration](https://kind.sigs.k8s.io/docs/user/configuration/) |
| 162 | as props: `nodes` with their `extraPortMappings`, `extraMounts`, and |
| 163 | labels, `networking`, `featureGates`, `runtimeConfig`, and the kubeadm |
| 164 | and containerd patches. Alchemy adds the containerd setting its |
| 165 | registry needs. |
| 166 | |
| 167 | Worker nodes pull from the registry too: |
| 168 | |
| 169 | ```typescript |
| 170 | const cluster = yield* Kubernetes.LocalCluster("Cluster", { |
| 171 | name: "alchemy", |
| 172 | nodes: [{ role: "control-plane" }, { role: "worker" }, { role: "worker" }], |
| 173 | }); |
| 174 | ``` |
| 175 | |
| 176 | kind can't change a running cluster, so changing any of these fields |
| 177 | replaces the cluster. |
| 178 | |
| 179 | ## Choose a Kubernetes version |
| 180 | |
| 181 | `nodeImage` selects the kind node image, and with it the Kubernetes |
| 182 | version: |
| 183 | |
| 184 | ```typescript |
| 185 | const cluster = yield* Kubernetes.LocalCluster("Cluster", { |
| 186 | name: "alchemy", |
| 187 | nodeImage: "kindest/node:v1.33.1", |
| 188 | }); |
| 189 | ``` |
| 190 | |
| 191 | Without it, kind uses its default node image. Changing `nodeImage` |
| 192 | replaces the cluster. |
| 193 | |
| 194 | ## Run several clusters |
| 195 | |
| 196 | Each cluster needs its own registry port: |
| 197 | |
| 198 | ```typescript |
| 199 | const blue = yield* Kubernetes.LocalCluster("Blue", { name: "blue" }); |
| 200 | const green = yield* Kubernetes.LocalCluster("Green", { |
| 201 | name: "green", |
| 202 | registryPort: 5002, |
| 203 | }); |
| 204 | ``` |
| 205 | |
| 206 | `registryPort` defaults to `5001`. Changing it recreates the registry |
| 207 | container on the new port, which drops the images pushed to it. |
| 208 | Workloads on the cluster are updated in the same deploy and push |
| 209 | their images to the new port. |
| 210 | |
| 211 | ## Updates and replacement |
| 212 | |
| 213 | - Changing `name`, `nodeImage`, `kubeconfig`, or any kind field |
| 214 | (`nodes`, `networking`, and so on) replaces the cluster. |
| 215 | Alchemy deletes the cluster and creates a new one. Everything |
| 216 | deployed to it is recreated on the new cluster. |
| 217 | - Changing `registryPort` moves the registry to the new port. |
| 218 | |
| 219 | If you delete the cluster outside Alchemy, for example with |
| 220 | `kind delete cluster`, [`alchemy drift`](/cli/drift) reports it as |
| 221 | missing. Run |
| 222 | `alchemy deploy --force` to recreate it and apply everything on it |
| 223 | again. |
| 224 | |
| 225 | ## Take it to a real cluster |
| 226 | |
| 227 | `LocalCluster` is for local work. The same workloads run on a hosted |
| 228 | cluster by changing what `cluster` points at. See |
| 229 | [Deploy to your own cluster](/kubernetes/tutorial/part-5). |
| 230 | |
| 231 | ## Where next |
| 232 | |
| 233 | - **[Tutorial](/kubernetes/tutorial/part-1).** Build up a stack on a |
| 234 | local cluster step by step. |
| 235 | - **[Container registries](/kubernetes/clusters/registries).** Learn |
| 236 | about the `registry` setting `LocalCluster` fills in for you. |
| 237 | - **[`LocalCluster` reference](/providers/kubernetes/localcluster).** |
| 238 | Look up every prop and attribute. |