Skip to main content

Deploy your first workload to Kubernetes

This tutorial deploys a small web application to a Cloider cluster with five standard Kubernetes objects: a Namespace, a ConfigMap, a Secret, a Deployment and a Service. You apply them with kubectl (or any tool in the tabs below), check that the pods are running, open the app, and finally publish it on a domain with HTTPS. Every manifest is complete and can be copied as is.

Before you begin​

The example app is podinfo, a tiny open source web server often used to test Kubernetes clusters. It listens on port 9898, shows a message you can configure, and has health endpoints for probes.

What you are going to create​

ObjectNameWhat it does
NamespaceshopA folder for the app's objects, so they don't mix with others
ConfigMapweb-configNon-secret settings, passed to the container as environment variables
Secretweb-secretsCredentials, passed to the container as environment variables
DeploymentwebRuns 2 replicas of the container and replaces them on updates
ServicewebA stable address in the cluster (web.shop.svc.cluster.local:80) in front of the pods

Step 1: Write the manifests​

Save the following as shop.yaml. The comments explain each part.

shop.yaml
# 1. A namespace for everything that belongs to the app
apiVersion: v1
kind: Namespace
metadata:
name: shop
---
# 2. Plain settings. Anyone who can read the namespace can read these
apiVersion: v1
kind: ConfigMap
metadata:
name: web-config
namespace: shop
data:
PODINFO_UI_MESSAGE: "Hello from Cloider Kubernetes"
PODINFO_UI_COLOR: "#34577c"
---
# 3. Credentials. stringData takes plain text; Kubernetes stores it base64 encoded
apiVersion: v1
kind: Secret
metadata:
name: web-secrets
namespace: shop
type: Opaque
stringData:
API_KEY: "change-me"
---
# 4. Two replicas of the container, with resources and health checks
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: shop
labels:
app: web
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: web
image: ghcr.io/stefanprodan/podinfo:6.7.0
ports:
- name: http
containerPort: 9898
envFrom:
- configMapRef:
name: web-config
- secretRef:
name: web-secrets
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 128Mi
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 5
periodSeconds: 10
---
# 5. A stable address for the pods, inside the cluster
apiVersion: v1
kind: Service
metadata:
name: web
namespace: shop
spec:
type: ClusterIP
selector:
app: web
ports:
- name: http
port: 80
targetPort: http
protocol: TCP

A few things to note:

  • Requests drive scheduling and billing. requests is what each container reserves. On Cloider every running container is billed on the larger of its request and its actual use, so set requests close to what the app really needs. Here 2 replicas reserve 100 millicores and 128 MiB in total. See How is a cluster billed?.
  • The memory limit protects you from leaks. A container that goes over limits.memory is restarted instead of growing forever.
  • Probes keep traffic away from broken pods. A pod only receives traffic from the Service once its readiness probe passes, and it is restarted when its liveness probe fails.
  • The Service is ClusterIP. It is only reachable inside the cluster. You publish it on a domain in step 5, or with a public IP in Load balancers.

Step 2: Apply the manifests​

  1. Open Kubernetes in the sidebar and click the cluster's name to open its terminal.
  2. Create the file in the terminal with a heredoc: type cat > shop.yaml <<'EOF', press Enter, paste the manifest, press Enter, type EOF and press Enter.
  3. Apply it:
kubectl apply -f shop.yaml
Output
namespace/shop created
configmap/web-config created
secret/web-secrets created
deployment.apps/web created
service/web created

The terminal's files are temporary, so keep the original shop.yaml on your computer or in git.

Step 3: Check that it is running​

Wait for the rollout to finish:

kubectl rollout status deployment/web -n shop
Output
deployment "web" successfully rolled out

List everything in the namespace:

kubectl get all -n shop
Output
NAME READY STATUS RESTARTS AGE
pod/web-6d8f7b9c5d-2kq9x 1/1 Running 0 40s
pod/web-6d8f7b9c5d-8hj2p 1/1 Running 0 40s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/web ClusterIP 10.43.118.27 <none> 80/TCP 40s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/web 2/2 2 2 40s

NAME DESIRED CURRENT READY AGE
replicaset.apps/web-6d8f7b9c5d 2 2 2 40s

Check that the ConfigMap and Secret reached the container:

kubectl exec -n shop deploy/web -- env | grep -E 'PODINFO_UI_MESSAGE|API_KEY'
Output
PODINFO_UI_MESSAGE=Hello from Cloider Kubernetes
API_KEY=change-me

Read the logs and the resource use:

kubectl logs -n shop deploy/web --tail 20
kubectl top pods -n shop

If a pod isn't Running, see Troubleshooting below.

Step 4: Open the app​

The Service is internal, so open it through a port forward from your computer (this needs a local kubectl with the kubeconfig):

kubectl port-forward -n shop service/web 8080:80

Then open http://localhost:8080. You see podinfo with the message from the ConfigMap. Press Ctrl-C to stop the port forward.

From the browser terminal, call the Service from inside the cluster instead:

kubectl run curl --rm -i --restart=Never --image=curlimages/curl:8.10.1 -n shop -- curl -s http://web.shop.svc.cluster.local/api/info

Step 5: Publish it on a domain​

To make the app reachable from the internet with HTTPS, route a domain to the web Service with a target. Claim a free subdomain such as shop-staging.cloider.app, then point it at the Service:

cloider domain create shop-staging.cloider.app
cloider target create shop-staging.cloider.app --cluster staging --service shop/web

A few moments later the app answers at https://shop-staging.cloider.app. Expose services on a domain covers targets in every tool, custom domains, paths and HTTPS. For a raw TCP port with a public IP, use a LoadBalancer Service instead.

Step 6: Update, scale and roll back​

Change the configuration. Edit PODINFO_UI_MESSAGE in shop.yaml and apply again. Environment variables are read when a container starts, so restart the pods to pick up the change:

kubectl apply -f shop.yaml
kubectl rollout restart deployment/web -n shop

Change the image. Set a new image tag; Kubernetes replaces the pods one by one, so the app stays up:

kubectl set image deployment/web -n shop web=ghcr.io/stefanprodan/podinfo:6.7.1
kubectl rollout status deployment/web -n shop

Roll back to the previous version if something is wrong:

kubectl rollout undo deployment/web -n shop

Scale by hand:

kubectl scale deployment/web -n shop --replicas 3

Scale automatically with a HorizontalPodAutoscaler. Every Cloider cluster runs the metrics server, so autoscaling on CPU works without extra setup:

web-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: web
namespace: shop
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: web
minReplicas: 2
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
kubectl apply -f web-hpa.yaml
kubectl get hpa -n shop

Utilization is measured against the CPU request, which is one more reason to set requests that match the real use. Each extra replica adds its request to the bill while it runs.

Step 7: Clean up​

Delete everything the tutorial created by deleting the namespace:

kubectl delete namespace shop

Deleting the namespace deletes the pods, the Service, the ConfigMap and the Secret, and the cluster stops billing for the pods' compute. If you created a target in step 5, delete it too with cloider target delete <target-id> (see cloider target list shop-staging.cloider.app -o wide), or delete the whole subdomain with cloider domain delete shop-staging.cloider.app.

Troubleshooting​

SymptomWhat to check
Pod stays Pendingkubectl describe pod -n shop <pod> and read Events. A request that no node can satisfy, or a PersistentVolumeClaim that isn't bound, keeps a pod pending
ImagePullBackOff or ErrImagePullThe image name or tag is wrong, or it is in a private registry without an imagePullSecret. See Pull private images
CrashLoopBackOffThe container starts and exits. Read kubectl logs -n shop <pod> --previous
OOMKilled in kubectl describe podThe container went over limits.memory. Raise the limit or fix the leak
Pods Running but 0/1 readyThe readiness probe fails. Check the probe's path and port, and the logs
The app works with port forward but not on the domainCheck the target's namespace, service and port with cloider target list <domain>. See Expose services

More cases are in Kubernetes troubleshooting.

Next steps​

Frequently asked questions

How do I deploy an app to a Cloider Kubernetes cluster?

Write standard Kubernetes manifests, for example a Deployment and a Service, and apply them with kubectl apply -f. You can also run kubectl in the browser terminal, or pipe the file through cloider cluster exec -i staging -- kubectl apply -f - without a local kubeconfig.

Do I need to set CPU and memory requests on Cloider?

It is strongly recommended. Each running container is billed on the larger of its request and its actual use, so a request close to the real use keeps the bill accurate, and the HorizontalPodAutoscaler measures utilization against the request.

How do I make my Kubernetes Service reachable from the internet?

Route a domain to the Service with a target, for example cloider target create shop-staging.cloider.app --cluster staging --service shop/web, which serves it over HTTPS. For a raw TCP or UDP port, use a Service of type LoadBalancer, which gets a public IP.

Does the HorizontalPodAutoscaler work on Cloider?

Yes. Every cluster runs the Kubernetes metrics server, so kubectl top and CPU based autoscaling work without extra setup.