| 1 | --- |
| 2 | title: Deployments |
| 3 | description: Run a replicated server on any Kubernetes cluster with Kubernetes.Deployment. Covers names, namespaces, Service types and URLs, replicas, resources, labels, the pod template escape hatch, and Effect servers. |
| 4 | --- |
| 5 | |
| 6 | [`Kubernetes.Deployment`](/providers/kubernetes/deployment) is a |
| 7 | replicated server, running a container image or an HTTP server |
| 8 | written in Effect. One resource applies three Kubernetes objects: |
| 9 | |
| 10 | - a **ServiceAccount** the pods run as, |
| 11 | - a **Deployment** that keeps `replicas` copies of the container |
| 12 | running, and |
| 13 | - a **Service** that gives them one stable address. |
| 14 | |
| 15 | ```typescript |
| 16 | const web = yield* Kubernetes.Deployment("Web", { |
| 17 | cluster, |
| 18 | name: "web", |
| 19 | namespace: "apps", |
| 20 | image: "ghcr.io/stefanprodan/podinfo:6.15.0", |
| 21 | port: 9898, |
| 22 | replicas: 3, |
| 23 | serviceType: "ClusterIP", |
| 24 | }); |
| 25 | ``` |
| 26 | |
| 27 | ## Names |
| 28 | |
| 29 | `name` is the name of all three objects. If you omit it, Alchemy |
| 30 | generates one from the stack, stage, and logical ID, lowercased and |
| 31 | limited to characters Kubernetes accepts. Either way the name is |
| 32 | fixed at creation. Later changes to `name` are ignored, which keeps |
| 33 | the Service address stable for everything that calls it. |
| 34 | |
| 35 | Every object also carries the label |
| 36 | `app.kubernetes.io/name: <name>`, which the Service uses to select |
| 37 | the pods. |
| 38 | |
| 39 | ## Namespaces |
| 40 | |
| 41 | `namespace` defaults to `default`. The namespace must already exist. |
| 42 | Create it in the same Stack with a |
| 43 | [Manifest](/kubernetes/objects/manifests) and pass the Manifest's |
| 44 | `name` attribute so Alchemy orders the Deployment after it: |
| 45 | |
| 46 | ```typescript |
| 47 | const ns = yield* Kubernetes.Manifest("AppsNamespace", { |
| 48 | cluster, |
| 49 | manifest: { apiVersion: "v1", kind: "Namespace", metadata: { name: "apps" } }, |
| 50 | }); |
| 51 | |
| 52 | const web = yield* Kubernetes.Deployment("Web", { |
| 53 | cluster, |
| 54 | namespace: ns.name, |
| 55 | image: "ghcr.io/stefanprodan/podinfo:6.15.0", |
| 56 | port: 9898, |
| 57 | }); |
| 58 | ``` |
| 59 | |
| 60 | Changing the namespace moves the workload. Alchemy applies the |
| 61 | objects in the new namespace and deletes them from the old one. |
| 62 | |
| 63 | ## Ports and Services |
| 64 | |
| 65 | `port` (default `3000`) is the container port and the Service port. |
| 66 | A Deployment exposes one port. `serviceType` picks how the Service |
| 67 | is reachable: |
| 68 | |
| 69 | - **`"LoadBalancer"`** is the default. The Service is reachable from |
| 70 | outside the cluster through a cloud load balancer. `url` is |
| 71 | `http://<hostname or IP>[:port]`. |
| 72 | - **`"NodePort"`** is reachable on each node's IP at a port |
| 73 | Kubernetes assigns. `url` is `undefined`. |
| 74 | - **`"ClusterIP"`** is reachable inside the cluster only. `url` is |
| 75 | `undefined`. |
| 76 | |
| 77 | For a `LoadBalancer` Service, the deploy waits up to about three |
| 78 | minutes for the cluster to publish the load balancer's hostname or |
| 79 | IP, then sets `url`. The port is part of the URL unless it's `80`. |
| 80 | If no address appears in time, the deploy still succeeds and `url` |
| 81 | is `undefined`. The next update picks it up. |
| 82 | |
| 83 | Clusters without a load balancer controller never assign an |
| 84 | address. That includes kind, minikube, and most bare-metal clusters. |
| 85 | Use `ClusterIP` or `NodePort` there, or install a controller such as |
| 86 | [cloud-provider-kind](https://github.com/kubernetes-sigs/cloud-provider-kind) |
| 87 | or [MetalLB](https://metallb.io). Inside the cluster, a Service is |
| 88 | reachable at `http://<serviceName>.<namespace>.svc.cluster.local:<port>`. |
| 89 | |
| 90 | Pass `serviceAnnotations` for load balancer settings such as scheme |
| 91 | or target type: |
| 92 | |
| 93 | ```typescript |
| 94 | serviceAnnotations: { |
| 95 | "service.beta.kubernetes.io/aws-load-balancer-scheme": "internal", |
| 96 | }, |
| 97 | ``` |
| 98 | |
| 99 | Some platforms set their own defaults for `LoadBalancer` Services. |
| 100 | On [Amazon EKS](/kubernetes/clusters/eks), the default is an |
| 101 | internet-facing Network Load Balancer. |
| 102 | |
| 103 | ## Replicas, resources, command, and args |
| 104 | |
| 105 | ```typescript |
| 106 | const api = yield* Kubernetes.Deployment("Api", { |
| 107 | cluster, |
| 108 | image: "ghcr.io/acme/api:v3", |
| 109 | port: 8080, |
| 110 | replicas: 3, |
| 111 | resources: { |
| 112 | requests: { cpu: "250m", memory: "256Mi" }, |
| 113 | limits: { memory: "512Mi" }, |
| 114 | }, |
| 115 | command: ["/app/server"], |
| 116 | args: ["--log-level", "info"], |
| 117 | }); |
| 118 | ``` |
| 119 | |
| 120 | `replicas` defaults to `1`. `resources` uses Kubernetes quantities. |
| 121 | `command` overrides the image's entrypoint and `args` its arguments. |
| 122 | |
| 123 | ## Environment variables |
| 124 | |
| 125 | `env` sets container environment variables. Values can be Outputs |
| 126 | from other resources, and non-string values are JSON-encoded. Alchemy |
| 127 | also sets `PORT`, `ALCHEMY_STACK_NAME`, `ALCHEMY_STAGE`, and |
| 128 | `ALCHEMY_PHASE`. Your `env` wins on conflicts. See |
| 129 | [Configuration & bindings](/kubernetes/workloads/bindings). |
| 130 | |
| 131 | ## Labels |
| 132 | |
| 133 | `labels` are merged over the generated `app.kubernetes.io/name` |
| 134 | label and applied to the Deployment, its pods, and the Service. They |
| 135 | are also part of the Deployment's pod selector: |
| 136 | |
| 137 | ```typescript |
| 138 | labels: { "app.kubernetes.io/part-of": "shop", tier: "frontend" }, |
| 139 | ``` |
| 140 | |
| 141 | Kubernetes doesn't allow a Deployment's selector to change, so set |
| 142 | `labels` before the first deploy. Changing them later fails with |
| 143 | `spec.selector: … field is immutable`. To change them anyway, give |
| 144 | the resource a new logical ID and a new `name`. Alchemy creates the |
| 145 | new Deployment and deletes the old one. For labels that shouldn't be |
| 146 | part of the selector, set them on the pod template instead, as |
| 147 | described in the next section. |
| 148 | |
| 149 | ## Tune the pod template |
| 150 | |
| 151 | `podTemplate` is an escape hatch for pod-level settings Alchemy |
| 152 | doesn't model. It's a partial Kubernetes |
| 153 | [`PodTemplateSpec`](https://kubernetes.io/docs/reference/kubernetes-api/workload-resources/pod-template-v1/#PodTemplateSpec) |
| 154 | merged over the generated one. Objects merge key by key, while |
| 155 | arrays and primitive values replace what was there. |
| 156 | |
| 157 | ```typescript |
| 158 | const api = yield* Kubernetes.Deployment("Api", { |
| 159 | cluster, |
| 160 | image: "ghcr.io/acme/api:v3", |
| 161 | port: 8080, |
| 162 | podTemplate: { |
| 163 | metadata: { |
| 164 | annotations: { "prometheus.io/scrape": "true" }, |
| 165 | }, |
| 166 | spec: { |
| 167 | nodeSelector: { "kubernetes.io/arch": "arm64" }, |
| 168 | tolerations: [{ key: "dedicated", operator: "Exists" }], |
| 169 | terminationGracePeriodSeconds: 30, |
| 170 | }, |
| 171 | }, |
| 172 | }); |
| 173 | ``` |
| 174 | |
| 175 | Good fits are pod metadata, `nodeSelector`, `affinity`, |
| 176 | `tolerations`, `topologySpreadConstraints`, `priorityClassName`, |
| 177 | pod `securityContext`, `imagePullSecrets`, and `volumes`. |
| 178 | Container-level fields live inside the `containers` array. That |
| 179 | includes probes, `volumeMounts`, `envFrom`, and a sidecar. An array |
| 180 | in `podTemplate` replaces the generated one wholesale, including the |
| 181 | image. When a workload needs full control of its containers, write |
| 182 | it as a [Manifest](/kubernetes/objects/manifests). |
| 183 | |
| 184 | ## Effect servers |
| 185 | |
| 186 | With `main`, the Deployment runs an Effect program instead of an |
| 187 | image. Alchemy bundles the module, builds an image, and serves the |
| 188 | returned `fetch` handler on `port`: |
| 189 | |
| 190 | ```typescript |
| 191 | // src/Api.ts |
| 192 | export default Kubernetes.Deployment( |
| 193 | "Api", |
| 194 | Effect.gen(function* () { |
| 195 | const cluster = yield* Cluster; |
| 196 | return { |
| 197 | cluster, |
| 198 | main: import.meta.url, |
| 199 | name: "api", |
| 200 | port: 3000, |
| 201 | replicas: 2, |
| 202 | serviceType: "ClusterIP" as const, |
| 203 | }; |
| 204 | }), |
| 205 | Effect.gen(function* () { |
| 206 | return { |
| 207 | fetch: Effect.gen(function* () { |
| 208 | const request = yield* HttpServerRequest; |
| 209 | return HttpServerResponse.text(`hello from ${request.url}`); |
| 210 | }), |
| 211 | }; |
| 212 | }), |
| 213 | ); |
| 214 | ``` |
| 215 | |
| 216 | The image is pushed to the cluster's registry. |
| 217 | [`LocalCluster`](/kubernetes/clusters/local) includes one. Other |
| 218 | clusters need a [`registry`](/kubernetes/clusters/registries) on the |
| 219 | connection, and EKS uses ECR. See |
| 220 | [Container images](/kubernetes/workloads/images). |
| 221 | |
| 222 | The tagged form declares the Deployment as a class, so other code |
| 223 | can depend on it by type and the implementation is provided as a |
| 224 | Layer: |
| 225 | |
| 226 | ```typescript |
| 227 | export class Api extends Kubernetes.Deployment<Api>()("Api") {} |
| 228 | |
| 229 | export default Api.make( |
| 230 | { cluster, main: import.meta.url, port: 3000 }, |
| 231 | Effect.gen(function* () { |
| 232 | return { |
| 233 | fetch: Effect.gen(function* () { |
| 234 | return HttpServerResponse.text("ok"); |
| 235 | }), |
| 236 | }; |
| 237 | }), |
| 238 | ); |
| 239 | ``` |
| 240 | |
| 241 | In the Stack, `yield* Api` and provide the default export with |
| 242 | `Effect.provide(ApiLive)`. This is the same authoring model as |
| 243 | [Lambda functions](/aws/compute/lambda) and |
| 244 | [Cloudflare Workers](/cloudflare/compute/workers). |
| 245 | |
| 246 | ## What a deploy waits for |
| 247 | |
| 248 | A deploy finishes when the API server has accepted the objects and, |
| 249 | for `LoadBalancer` Services, when the load balancer address is known. |
| 250 | It doesn't wait for the rollout. Pods may still be pulling images or |
| 251 | failing health checks. Check with `kubectl rollout status`, or add a |
| 252 | smoke-test [Job](/kubernetes/workloads/jobs) that calls the Service. |
| 253 | |
| 254 | ## Updates and replacement |
| 255 | |
| 256 | - **`image`, `env`, `replicas`, `resources`, `podTemplate`, and other |
| 257 | props.** Changing these is an update. The objects are re-applied |
| 258 | and Kubernetes rolls the pods. |
| 259 | - **Files under `context`, or the `main` program.** Changing these is |
| 260 | an update, detected from the image content hash. |
| 261 | - **`namespace`.** The objects move to the new namespace. |
| 262 | - **`cluster`.** Moving to a different connection is a replacement. |
| 263 | See |
| 264 | [Move a resource to another cluster](/kubernetes/clusters/connecting#move-a-resource-to-another-cluster). |
| 265 | - **`labels`.** Kubernetes rejects the change. See [Labels](#labels). |
| 266 | - **`name`.** Changes are ignored after creation. |
| 267 | |
| 268 | ## Attributes |
| 269 | |
| 270 | - `deploymentName` is the name of the Kubernetes Deployment. |
| 271 | - `serviceName` is the name of the Service. |
| 272 | - `serviceAccountName` is the name of the ServiceAccount the pods |
| 273 | run as. |
| 274 | - `namespace` is the namespace of all three objects. |
| 275 | - `port` is the container and Service port. |
| 276 | - `url` is the load balancer URL for `LoadBalancer` Services, and |
| 277 | `undefined` otherwise. |
| 278 | - `imageUri` is the image the pods run. |
| 279 | - `connection` is the cluster connection. |
| 280 | - `kubernetesObjects` holds references to the applied objects. |
| 281 | |
| 282 | ## Where next |
| 283 | |
| 284 | - **[Container images](/kubernetes/workloads/images).** Covers |
| 285 | `image`, `context`, and `main` on each kind of cluster. |
| 286 | - **[Jobs & CronJobs](/kubernetes/workloads/jobs).** Run-to-completion |
| 287 | work with the same props. |
| 288 | - **[`Deployment` reference](/providers/kubernetes/deployment).** |
| 289 | Lists every prop and attribute. |