# Deploy your first app on Kubernetes

Source: https://docs.quake.ai/resources/deployments/deploy-first-app
Markdown: https://docs.quake.ai/resources/deployments/deploy-first-app.md
> Build a managed Kubernetes cluster on Quake AI from a fresh project, deploy an app with kubectl, and expose it to the internet with a load-balancer service.

---

# Deploy your first app on Kubernetes

Stand up a Magnum Kubernetes cluster on Quake AI and deploy an Nginx app reachable from the public internet through a `LoadBalancer` service. The Kubernetes service (OpenStack Magnum) provisions the control plane, worker nodes, and cluster networking; you drive the cluster with `kubectl` once it reaches `CREATE_COMPLETE`.

<PricingCompanion components={[{ kind: "template", slug: "k8s-cluster", required: true }]} />

<Figure caption="Kubernetes cluster with a LoadBalancer service: traffic from the internet reaches Nginx pods through the service's public IP">

```d2
direction: right

laptop: Your laptop {shape: person}
public: Public IP\n203.0.113.x

cloud: Quake AI project {
  cluster: Kubernetes cluster {
    master: Master node\ncontrol plane
    worker: Worker node {
      svc: Service\ntype LoadBalancer
      pods: Nginx pods
    }
  }
}

laptop -> public: HTTP 80
public -> cloud.cluster.worker.svc
cloud.cluster.worker.svc -> cloud.cluster.worker.pods
```

</Figure>

## Prerequisites

You need:

<DeveloperPlanPrereqs />

- `kubectl` installed on your local machine. See the [Kubernetes install docs](https://kubernetes.io/docs/tasks/tools/) for your operating system.
- A cluster template in your project. Step 1 covers how to confirm you have one. To build your own, follow [How to create a cluster template](/docs/kubernetes/how-to/create-cluster-template) first, then return here.

Use the Console to create the cluster and download credentials; use your local terminal for `kubectl` steps.



A Kubernetes cluster consumes more of your project quota than a single VM, and a `LoadBalancer` service allocates a public IP. A minimal one-master, one-worker cluster plus one public service fits a default-tier project. If you run other workloads in the same project, check your compute and floating-IP quota before you start. The [create-cluster how-to](/docs/kubernetes/how-to/create-cluster) covers the quota math.



## Step 1: Confirm you have a cluster template

A cluster template defines the Kubernetes version, the network driver, and the default node flavors that the cluster inherits. You select a template when you create the cluster.

1. In the Console, go to **Kubernetes** > **Cluster Templates**.
2. Look for a platform-provided template in the list. Platform templates include the Kubernetes version in the name, for example `Standard-v2.0-k8s-calico-fc38_v1.24.16`.

If the list is empty, create a template before continuing: follow [How to create a cluster template](/docs/kubernetes/how-to/create-cluster-template), then come back to Step 2. That guide also documents the labels a template needs (including `boot_volume_size`, which Quake AI requires because every flavor family ships with a zero-size root disk).

## Step 2: Create the cluster

Create a one-master, one-worker cluster from the template. The Console renders the Create Cluster wizard as five numbered steps.

1. Go to **Kubernetes** > **Clusters** and select **Create Cluster**.
2. **Step 1: Cluster Info.** Set **Cluster Name** to `tutorial-k8s`. Under **Cluster Template**, select the platform template you found in Step 1. Select **Next: Node Spec**.
3. **Step 2: Node Spec.** Select your SSH **Keypair**. Leave **Number of Master Nodes** at `1` and **Number of Nodes** at `1` for this walkthrough. Pick master and worker flavors from the families listed in the dropdown (for example, `m2a.xlarge` for the master and `m2a.large` for the worker). Available families vary by project; choose any flavor that fits your quota. Select **Next: Network Setting**.
4. **Step 3: Network Setting.** Under **Enable Load Balancer**, check **Enabled Load Balancer for Master Nodes** so the cluster API gets a load balancer. Under **Enabled Network**, leave **Create New Network** checked so the wizard provisions a dedicated cluster network. Select **Next: Management**.
5. **Step 4: Management.** Leave the defaults. Select **Next: Additional Labels**.
6. **Step 5: Additional Labels.** Confirm the template carries `boot_volume_size=40`. If the label is missing, add it with **+ Add Label** (key `boot_volume_size`, value `40`). Without it, the cluster fails within about a minute because the flavors have a zero-size root disk.
7. Select **Confirm**.

The cluster appears in the **Clusters** list with status **CREATE_IN_PROGRESS**. Provisioning a small cluster takes 5 to 15 minutes. Wait until the status reads **CREATE_COMPLETE** before continuing.



A single-master cluster cannot add masters later. For a cluster you intend to keep, create it with 3 master nodes from the start. See [How to create a Kubernetes cluster](/docs/kubernetes/how-to/create-cluster) for production sizing guidance.



## Step 3: Connect with kubectl

Download the cluster's kubeconfig from the Console and point `kubectl` at it.

1. Go to **Kubernetes** > **Clusters**.
2. On the `tutorial-k8s` row, open the **Settings** gear icon and select **Get kube.config**. The browser downloads a plain-text YAML file immediately.
3. Move the file to a known location and export it as `KUBECONFIG` in your terminal:

```bash
export KUBECONFIG=/path/to/downloaded/kube.config
```

4. Confirm `kubectl` reaches the cluster:

```bash
kubectl get nodes
```

Expected output for a one-master, one-worker cluster:

```text
NAME                                   STATUS   ROLES    AGE   VERSION
tutorial-k8s-<token>-master-0          Ready    master   9m    v1.24.16
tutorial-k8s-<token>-node-0            Ready    <none>   7m    v1.24.16
```

Both nodes show `Ready`. The Kubernetes service injects a random token into the node names to keep them unique across cluster re-creates, so your exact names differ.

## Step 4: Deploy the app

Define an Nginx Deployment and apply it with `kubectl`. Save the following manifest as `app.yaml` on your local machine:

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: hello-nginx
  template:
    metadata:
      labels:
        app: hello-nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27
          ports:
            - containerPort: 80
```

Apply it:

```bash
kubectl apply -f app.yaml
```

Expected output:

```text
deployment.apps/hello-nginx created
```

Watch the pods until both reach `Running`:

```bash
kubectl get pods
```

Expected output:

```text
NAME                           READY   STATUS    RESTARTS   AGE
hello-nginx-7c8b5d9f4b-5n2hq   1/1     Running   0          40s
hello-nginx-7c8b5d9f4b-pq7xz   1/1     Running   0          40s
```

The first run pulls the `nginx:1.27` image, so the pods may sit in `ContainerCreating` for a few seconds before they reach `Running`.

## Step 5: Expose the app to the internet

A `LoadBalancer` service tells the cluster to publish the selected pods through a public IP. Add the service to `app.yaml` below the Deployment, separated by a `---` document break:

```yaml
---
apiVersion: v1
kind: Service
metadata:
  name: hello-nginx
spec:
  type: LoadBalancer
  selector:
    app: hello-nginx
  ports:
    - port: 80
      targetPort: 80
```

Apply the file again. `kubectl` creates the service and leaves the Deployment unchanged:

```bash
kubectl apply -f app.yaml
```

Expected output:

```text
deployment.apps/hello-nginx unchanged
service/hello-nginx created
```

Watch the service until the `EXTERNAL-IP` column changes from `<pending>` to an address:

```bash
kubectl get service hello-nginx --watch
```

Expected output once the public service finishes provisioning:

```text
NAME          TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)        AGE
hello-nginx   LoadBalancer   10.254.12.118   203.0.113.42    80:31840/TCP   2m
```

The address in `EXTERNAL-IP` is the public IP the cluster allocated for the service. Provisioning the service and assigning the IP takes 2 to 4 minutes. Press `Ctrl+C` to stop watching once the IP appears.



The `LoadBalancer` service consumes one floating IP from your project. If the `EXTERNAL-IP` stays `<pending>` for more than a few minutes, check your project's floating-IP quota.



## Step 6: Reach the app

Send a request to the public IP from your local machine. Replace `203.0.113.42` with the `EXTERNAL-IP` from Step 5:

```bash
curl http://203.0.113.42
```

Expected output (the start of the default Nginx welcome page):

```html
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
```

The request travels from your machine to the service's public IP and then to one of the two Nginx pods on the worker node.

## Next steps

- [How to deploy to a Quake AI Kubernetes cluster from CI](/docs/kubernetes/how-to/deploy-from-ci): automate deploys after you outgrow manual `kubectl apply`
- [How to use a container registry with Quake AI](/docs/kubernetes/how-to/use-container-registry): private images for production workloads
- [How to manage a Kubernetes cluster](/docs/kubernetes/how-to/manage-cluster): scale workers, resize, and delete clusters
- [How to create a cluster template](/docs/kubernetes/how-to/create-cluster-template): define the version, network driver, and node defaults a cluster inherits
- [Kubernetes on Quake AI](/docs/kubernetes/concepts/kubernetes): the cluster architecture and resource planning behind the Kubernetes service
- [Kubernetes CLI reference](/reference/kubernetes/cli): the `openstack coe` commands for clusters and templates
- [All deployments](/resources/deployments): the full catalog of hands-on guides across compute, networking, storage, and AI

## Clean up

To stop using project resources, delete the app and the cluster.

1. **Delete the app.** Remove the Deployment and the `LoadBalancer` service, which releases its public IP:

```bash
kubectl delete -f app.yaml
```

2. **Delete the cluster.** Go to **Kubernetes** > **Clusters**, open the **Settings** gear icon on the `tutorial-k8s` row, and select **Delete**. Confirm in the dialog. Read the cluster name in the dialog body first; the Console deletes on a single confirmation without asking you to type the name.

Deleting the cluster removes its VMs, network, router, and cluster networking resources. The cluster leaves the list once deletion finishes.
