| 1 | --- |
| 2 | title: "Part 5: Deploy to Your Own Cluster" |
| 3 | description: Point the tutorial stack at a hosted Kubernetes cluster such as GKE, AKS, EKS, k3s, or anything in your kubeconfig. Keep the local cluster for development with stages, and clean up. |
| 4 | sidebar: |
| 5 | order: 5 |
| 6 | --- |
| 7 | |
| 8 | import Terminal from "../../../../components/Terminal.astro"; |
| 9 | import { Code, Tabs, TabItem } from "@astrojs/starlight/components"; |
| 10 | |
| 11 | Everything you built in Parts 1–4 runs on a cluster on your machine. |
| 12 | None of it is specific to that cluster. The Deployment, the Effect |
| 13 | Jobs, and the Helm chart only need a `cluster` to run on. In this part |
| 14 | you'll point the stack at a cluster of your own, while keeping the |
| 15 | local one for development. |
| 16 | |
| 17 | ## Choose a cluster |
| 18 | |
| 19 | You need a cluster you can reach with `kubectl`. If you don't have |
| 20 | one, create it with your platform's tools and follow its section of |
| 21 | [Setup](/kubernetes/setup#connect-to-your-cluster): |
| 22 | |
| 23 | - **GKE.** `gcloud container clusters get-credentials` writes the |
| 24 | context. See [Setup → GKE](/kubernetes/setup#gke). |
| 25 | - **AKS.** `az aks get-credentials` writes the context. See |
| 26 | [Setup → AKS](/kubernetes/setup#aks). |
| 27 | - **k3s, RKE2, DigitalOcean, and others.** Use the kubeconfig the |
| 28 | platform gives you. See |
| 29 | [Setup → k3s, RKE2, and other distributions](/kubernetes/setup#k3s-rke2-and-other-distributions). |
| 30 | - **Amazon EKS.** Create the cluster in this same Stack. See |
| 31 | [Amazon EKS](/kubernetes/clusters/eks). |
| 32 | |
| 33 | Then note its context name: |
| 34 | |
| 35 | ```sh |
| 36 | kubectl config get-contexts |
| 37 | ``` |
| 38 | |
| 39 | The rest of this part uses a context named `prod`. |
| 40 | |
| 41 | ## Choose a registry |
| 42 | |
| 43 | The smoke test and health check are built from your code, so they |
| 44 | need a registry the cluster's nodes can pull from. The local cluster |
| 45 | came with one. For yours, pick a repository you can push to, such as |
| 46 | `ghcr.io/<you>` on GitHub, and log in: |
| 47 | |
| 48 | ```sh |
| 49 | echo $GITHUB_TOKEN | docker login ghcr.io -u <you> --password-stdin |
| 50 | ``` |
| 51 | |
| 52 | Make the pushed images readable by the cluster. Use a public package, |
| 53 | or [pull credentials](/kubernetes/clusters/registries#let-the-nodes-pull) |
| 54 | for a private one. See [Container registries](/kubernetes/clusters/registries). |
| 55 | |
| 56 | ## Pick the cluster by stage |
| 57 | |
| 58 | Every Alchemy deploy targets a **stage**. By default that's your own |
| 59 | `live_<you>` stage, or you can name one with `--stage`. Keep the |
| 60 | local cluster for your stage and use the hosted cluster for `prod`. In `src/infra.ts`, |
| 61 | replace the `Cluster` declaration: |
| 62 | |
| 63 | ```diff lang="typescript" |
| 64 | +import { Stage } from "alchemy"; |
| 65 | import * as Kubernetes from "alchemy/Kubernetes"; |
| 66 | import * as Effect from "effect/Effect"; |
| 67 | |
| 68 | -export const Cluster = Kubernetes.LocalCluster("Cluster", { |
| 69 | - name: "alchemy", |
| 70 | -}); |
| 71 | +export const Cluster = Effect.gen(function* () { |
| 72 | + const stage = yield* Stage; |
| 73 | + if (stage === "prod") { |
| 74 | + return Kubernetes.KubeConfig({ |
| 75 | + context: "prod", |
| 76 | + registry: { server: "ghcr.io/you" }, |
| 77 | + }); |
| 78 | + } |
| 79 | + return yield* Kubernetes.LocalCluster("Cluster", { name: "alchemy" }); |
| 80 | +}); |
| 81 | ``` |
| 82 | |
| 83 | For `prod`, `Kubernetes.KubeConfig` connects through the `prod` |
| 84 | context and pushes images to `ghcr.io/you`. Every other stage keeps |
| 85 | the local cluster. Nothing else changes. The Namespace, Deployment, |
| 86 | Jobs, and chart all take whichever cluster `Cluster` returns. |
| 87 | |
| 88 | ## Set the node architecture |
| 89 | |
| 90 | Images are built for `amd64` nodes by default. If your cluster runs |
| 91 | Arm nodes, such as AWS Graviton or Ampere, say so: |
| 92 | |
| 93 | ```diff lang="typescript" |
| 94 | return Kubernetes.KubeConfig({ |
| 95 | context: "prod", |
| 96 | registry: { server: "ghcr.io/you" }, |
| 97 | + architecture: "arm64", |
| 98 | }); |
| 99 | ``` |
| 100 | |
| 101 | ## Stop returning the local context |
| 102 | |
| 103 | `cluster.context` only exists on the local cluster. Remove it from |
| 104 | the Stack's outputs in `alchemy.run.ts`: |
| 105 | |
| 106 | ```diff lang="typescript" |
| 107 | return { |
| 108 | - context: cluster.context, |
| 109 | namespace: web.namespace, |
| 110 | service: web.serviceName, |
| 111 | smokeTest: smokeTest.jobName, |
| 112 | }; |
| 113 | ``` |
| 114 | |
| 115 | ## Deploy to prod |
| 116 | |
| 117 | <Tabs syncKey="pkgManager"> |
| 118 | <TabItem label="bun" icon="bun"> |
| 119 | <Code code="bun alchemy deploy --stage prod" lang="sh" /> |
| 120 | </TabItem> |
| 121 | <TabItem label="npm" icon="npm"> |
| 122 | <Code code="npm run alchemy deploy -- --stage prod" lang="sh" /> |
| 123 | </TabItem> |
| 124 | <TabItem label="pnpm" icon="pnpm"> |
| 125 | <Code code="pnpm alchemy deploy --stage prod" lang="sh" /> |
| 126 | </TabItem> |
| 127 | <TabItem label="yarn" icon="seti:yarn"> |
| 128 | <Code code="yarn alchemy deploy --stage prod" lang="sh" /> |
| 129 | </TabItem> |
| 130 | </Tabs> |
| 131 | |
| 132 | <Terminal content={`[u]Plan[/u]: [g]5 to create[/g] |
| 133 | |
| 134 | [g]+[/g] [b]HealthCheck[/b] [d](Kubernetes.Job)[/d] |
| 135 | [g]+[/g] [b]MetricsServer[/b] [d](Kubernetes.HelmChart)[/d] |
| 136 | [g]+[/g] [b]Namespace[/b] [d](Kubernetes.Manifest)[/d] |
| 137 | [g]+[/g] [b]SmokeTest[/b] [d](Kubernetes.Job)[/d] |
| 138 | [g]+[/g] [b]Web[/b] [d](Kubernetes.Deployment)[/d] |
| 139 | |
| 140 | Proceed? |
| 141 | ◉ Yes ○ No |
| 142 | [d]Pushed ghcr.io/you/mycluster-smoketest-prod-…:7a31fba1…[/d] |
| 143 | [d]Pushed ghcr.io/you/mycluster-healthcheck-prod-…:27bde98e…[/d] |
| 144 | [g]✓[/g] [b]Namespace[/b] [d](Kubernetes.Manifest)[/d] [g]created[/g] |
| 145 | [g]✓[/g] [b]MetricsServer[/b] [d](Kubernetes.HelmChart)[/d] [g]created[/g] |
| 146 | [g]✓[/g] [b]Web[/b] [d](Kubernetes.Deployment)[/d] [g]created[/g] |
| 147 | [g]✓[/g] [b]SmokeTest[/b] [d](Kubernetes.Job)[/d] [g]created[/g] |
| 148 | [g]✓[/g] [b]HealthCheck[/b] [d](Kubernetes.Job)[/d] [g]created[/g]`} /> |
| 149 | |
| 150 | `prod` has its own state, so this deploy creates everything on the |
| 151 | hosted cluster and leaves your local stage alone. There's no |
| 152 | `LocalCluster` in the plan, because the `prod` stage never declares |
| 153 | one. |
| 154 | |
| 155 | ## Check the hosted cluster |
| 156 | |
| 157 | The same commands work, with the `prod` context: |
| 158 | |
| 159 | ```sh |
| 160 | kubectl --context prod -n my-app get deployments,jobs,cronjobs |
| 161 | kubectl --context prod -n my-app logs job/<smokeTest> |
| 162 | ``` |
| 163 | |
| 164 | ## Skip what the platform already has |
| 165 | |
| 166 | Many hosted clusters include metrics-server, and applying the chart |
| 167 | over it would take ownership of its objects. Install the chart only |
| 168 | where it's missing: |
| 169 | |
| 170 | ```diff lang="typescript" |
| 171 | +import { Stage } from "alchemy"; |
| 172 | import * as Kubernetes from "alchemy/Kubernetes"; |
| 173 | ``` |
| 174 | |
| 175 | ```diff lang="typescript" |
| 176 | yield* HealthCheck; |
| 177 | |
| 178 | -const metricsServer = yield* Kubernetes.HelmChart("MetricsServer", { |
| 179 | - cluster, |
| 180 | - // ... |
| 181 | -}); |
| 182 | +const stage = yield* Stage; |
| 183 | +if (stage !== "prod") { |
| 184 | + yield* Kubernetes.HelmChart("MetricsServer", { |
| 185 | + cluster, |
| 186 | + // ... |
| 187 | + }); |
| 188 | +} |
| 189 | ``` |
| 190 | |
| 191 | Deploying `prod` again now plans the chart for deletion there. |
| 192 | |
| 193 | ## Clean up the local cluster |
| 194 | |
| 195 | Destroy your own stage to remove everything on your machine, |
| 196 | including the kind cluster and its registry: |
| 197 | |
| 198 | <Tabs syncKey="pkgManager"> |
| 199 | <TabItem label="bun" icon="bun"> |
| 200 | <Code code="bun alchemy destroy" lang="sh" /> |
| 201 | </TabItem> |
| 202 | <TabItem label="npm" icon="npm"> |
| 203 | <Code code="npm run alchemy destroy" lang="sh" /> |
| 204 | </TabItem> |
| 205 | <TabItem label="pnpm" icon="pnpm"> |
| 206 | <Code code="pnpm alchemy destroy" lang="sh" /> |
| 207 | </TabItem> |
| 208 | <TabItem label="yarn" icon="seti:yarn"> |
| 209 | <Code code="yarn alchemy destroy" lang="sh" /> |
| 210 | </TabItem> |
| 211 | </Tabs> |
| 212 | |
| 213 | Alchemy deletes the resources in reverse dependency order and the |
| 214 | cluster last. The next `alchemy deploy` recreates it in about 30 |
| 215 | seconds. Run `alchemy destroy --stage prod` when you want to remove |
| 216 | the hosted deployment too. |
| 217 | |
| 218 | ## Recap |
| 219 | |
| 220 | You started with an empty directory and now have: |
| 221 | |
| 222 | - A stack that runs on a local cluster for development and on a |
| 223 | hosted cluster in `prod` |
| 224 | - Effect programs built and pushed to whichever registry the cluster |
| 225 | uses |
| 226 | - A Deployment, a Helm chart, and Jobs that don't depend on where |
| 227 | they run |
| 228 | |
| 229 | ## Where next |
| 230 | |
| 231 | - **[Setup](/kubernetes/setup).** Connect to more kinds of clusters, |
| 232 | check RBAC permissions, and deploy from CI. |
| 233 | - **[Container registries](/kubernetes/clusters/registries).** Push |
| 234 | credentials, pull access, and node architecture. |
| 235 | - **[Deployments](/kubernetes/workloads/deployments).** Effect HTTP |
| 236 | servers, Service types, and the pod template escape hatch. |
| 237 | - **[Jobs & CronJobs](/kubernetes/workloads/jobs).** Everything about |
| 238 | image and Effect Jobs. |
| 239 | - **[Stages](/environments/stages).** Per-developer, preview, and |
| 240 | production stages. |
| 241 | - **[Amazon EKS](/kubernetes/clusters/eks).** Create the cluster in |
| 242 | the same Stack and use AWS bindings. |