Building an On-Premise Kubernetes Cluster โ€” Part 6: Deploying, Updating, and Scaling Your Own Application

์ž‘์„ฑ์ž

์นดํ…Œ๊ณ ๋ฆฌ:

โ† ํ”ผ๋“œ๋กœ
DEV Community ยท Celso Nery ยท 2026-07-30 ๊ฐœ๋ฐœ(SW)

๐Ÿ‡ง๐Ÿ‡ท Leia a versรฃo em portuguรชs aqui

In Part 5 of this series, we validated the cluster end to end by deploying Nginx. Now let’s go one step further: build a custom application’s Docker image, publish it, get it running in the cluster, and explore day-to-day operations โ€” version updates, rollback, and scalability (both manual and automatic).

As an example, we used a simple REST API (myapp.war), built with Spring Boot, purely for illustration โ€” the process applies to any application packaged as a container image.

Building the application’s Docker image

The first step is writing the application’s Dockerfile. In this example, a lightweight base image (alpine) was used, with Java 11 installed to run the application:

FROM alpine
WORKDIR /opt/app
RUN apk update && apk add vim openjdk11-jre
COPY runapp.sh .
CMD ash runapp.sh

Enter fullscreen mode Exit fullscreen mode

Building the image

docker image build -t oregontecnologia/myapp-api:1.0.0 .

Enter fullscreen mode Exit fullscreen mode

Publishing the image

Before using the image in the cluster, it needs to be available in some registry โ€” either Docker Hub or a private registry. If you’d rather host your own on-premise registry (recommended for corporate environments or those without internet access), check out the companion article on creating a local registry server.

To publish to Docker Hub:

docker login

username:
password:

docker push oregontecnologia/myapp-api:1.0.0

Enter fullscreen mode Exit fullscreen mode

Deploying the application

With the image published, you can check the cluster’s current state before proceeding:

kubectl get pods -o wide
kubectl get deploy -o wide

Enter fullscreen mode Exit fullscreen mode

Create the Deployment directly from the command line, pointing to the published image:

kubectl create deploy myapp-deploy --image=oregontecnologia/myapp-api:1.0.0

Enter fullscreen mode Exit fullscreen mode

Unlike previous examples in this series (where we used YAML files with kubectl apply -f), here the Deployment is created directly via the command line with kubectl create deploy. Both approaches are valid โ€” YAML files are more suitable when you need to version and consistently reapply configurations.

Exposing the application externally

To make the application accessible outside the cluster, create a LoadBalancer-type Service, associating a fixed external IP:

kubectl expose deploy myapp-deploy --type=LoadBalancer --external-ip=10.0.10.100 --port=80

Enter fullscreen mode Exit fullscreen mode

In on-premise environments, the LoadBalancer type usually doesn’t provision a load balancer automatically (that’s a native feature of cloud providers). That’s why here we manually provide an external IP (--external-ip) already available on the local network.

Removing the application

If you need to remove the Deployment:

kubectl delete deploy myapp-deploy

Enter fullscreen mode Exit fullscreen mode

Updating and rolling back application versions

One of Kubernetes’ great advantages is managing updates in a controlled way, with no perceptible downtime (rolling update).

Updating to a new version

kubectl set image deployments/myapp-deploy myapp-api=oregontecnologia/myapp-api:1.0.2 --record

Enter fullscreen mode Exit fullscreen mode

This command updates the myapp-api container’s image inside the Deployment to version 1.0.2. The --record flag records the command in the Deployment’s revision history, which makes it easier to later identify what changed in each rollout.

Rolling back to the previous version

If the new version causes problems, you can quickly roll back to the previous revision:

kubectl rollout undo deployments/myapp-deploy

Enter fullscreen mode Exit fullscreen mode

Scaling the application

Manual scaling

To manually adjust the number of running replicas โ€” for example, to 3:

kubectl scale deploy myapp-deploy --replicas=3

Enter fullscreen mode Exit fullscreen mode

Automatic scaling (HPA)

To let Kubernetes itself automatically adjust the number of replicas based on CPU usage, you can configure a Horizontal Pod Autoscaler (HPA). In the example below, the cluster starts with 2 replicas and can scale up to 10, whenever average CPU utilization exceeds 75%:

kubectl autoscale deploy myapp-deploy --min=2 --max=10 --cpu-percent=75

Enter fullscreen mode Exit fullscreen mode

Adjust the --min, --max, and --cpu-percent values according to your cluster’s actual capacity and your application’s expected behavior.

To check the autoscaler’s current state:

kubectl get hpa

Enter fullscreen mode Exit fullscreen mode

This command shows, among other things, the current CPU usage relative to the configured target, and the number of replicas currently running.

To remove the autoscaler:

kubectl delete hpa myapp-deploy

Enter fullscreen mode Exit fullscreen mode

Now, let’s put this entire manual process into a YAML file to make things easier.

Throughout this series, we’ve used several .yaml files to create Deployments, Services, and other objects in the cluster. In this article, let’s take a step back and understand the basic structure of these manifests โ€” what’s mandatory in every file, how to find the correct apiVersion, and what the main object types we’ve already used look like in practice: Pod, ReplicaSet, Deployment, and Service.

The minimum structure of a manifest

Every Kubernetes object, regardless of type, is described by a YAML with four main fields:

apiVersion: 
kind: 
metadata: 
spec: 

Enter fullscreen mode Exit fullscreen mode

  • apiVersion: the Kubernetes API version used to create that object (e.g., v1 or apps/v1);
  • kind: the type of object being created (Pod, Deployment, Service, etc.);
  • metadata: the object’s metadata โ€” name, labels, namespace, and more;
  • spec: the actual specification of the object โ€” for a Pod, for example, this is where the containers live.

How to find the correct apiVersion

Each object type (kind) belongs to a specific apiVersion, and this can vary between Kubernetes versions. To check which resources are available in your cluster and which API each one belongs to, use:

$ kubectl api-resources

Enter fullscreen mode Exit fullscreen mode

This command lists all resources supported by the cluster, along with the corresponding API group (the APIVERSION column) โ€” it’s the most reliable source for knowing which apiVersion to use in each manifest, since this can change depending on the installed Kubernetes version.

Watch out for indentation

YAML is a format sensitive to indentation โ€” unlike {} braces or [] brackets, the data hierarchy is defined purely by spacing. Because of this:

  • Never use TABs to indent a YAML file โ€” always use spaces.
  • The recommended standard (and the one used in the examples below) is 2 spaces per indentation level.
  • metadata and spec are mappings (key-value pairs), not lists โ€” meaning their internal fields (name, labels, etc.) should not start with a -.

This last point is a common mistake: it’s easy to confuse lists (which use -) with simple mappings. In the Pod example below, notice that name and labels sit directly under metadata, with no dash.

Pod

The most basic Kubernetes object is the Pod โ€” the smallest unit that can be created and managed in the cluster, containing one or more containers.

apiVersion: v1
kind: Pod
metadata:
  name: mypod
  labels:
    app: mypod-label
spec:
  containers:
    - name: myapp-api
      image: oregontecnologia/myapp-api:1.0.2

Enter fullscreen mode Exit fullscreen mode

Creating the object: create vs apply

There are two main commands for applying a manifest to the cluster:

$ kubectl create -f file.yaml
$ kubectl apply -f file.yaml

Enter fullscreen mode Exit fullscreen mode

The difference between them matters:

  • create only creates the object if it doesn’t already exist โ€” if the object was already created previously, the command returns an error.
  • apply creates the object if it doesn’t exist, or updates the existing object if there’s any change in the manifest. Because of this, apply is the most commonly used command day-to-day, since it lets you reapply the same file continuously as it evolves.

Checking the creation

$ kubectl get pods

Enter fullscreen mode Exit fullscreen mode

The -o wide flag shows additional information about the running pod, such as the node it’s running on and its internal IP.

To see all details of a pod or deployment (including recent events, which helps a lot when debugging issues):

$ kubectl describe pod myapp-api

Enter fullscreen mode Exit fullscreen mode

Or, for a deployment:

$ kubectl describe deploy myapp-deploy

Enter fullscreen mode Exit fullscreen mode

ReplicaSet

A single Pod doesn’t automatically recover if it fails. That’s where the ReplicaSet comes in: an object responsible for ensuring a defined number of replicas of the same Pod is always running.

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: myreplicaset
spec:
  replicas: 5
  selector:
    matchLabels:
      app: mypod-label
  template:
    metadata:
      labels:
        app: mypod-label
    spec:
      containers:
        - name: myapp-api
          image: oregontecnologia/myapp-api:1.0.2

Enter fullscreen mode Exit fullscreen mode

Notice the structure is a bit more nested than the Pod’s: inside spec, we have the selector (which defines which Pods belong to this ReplicaSet, based on labels) and the template (which describes how each replica of the Pod should be created).

Checking the creation:

$ kubectl get replicaset

Enter fullscreen mode Exit fullscreen mode

In practice, it’s rare to create ReplicaSets directly โ€” they’re normally managed automatically by a Deployment, which we’ll cover next. Still, understanding this layer helps in understanding how Kubernetes guarantees Pod availability.

Deployment

The Deployment is the most common way to run applications on Kubernetes. It manages ReplicaSets automatically, adding features like rolling updates and rollback โ€” which we already saw in practice in Part 6 of this series.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: mydeploy
spec:
  replicas: 5
  selector:
    matchLabels:
      app: mypod-label
  template:
    metadata:
      labels:
        app: mypod-label
    spec:
      containers:
        - name: myapp-api
          image: oregontecnologia/myapp-api:1.0.2

Enter fullscreen mode Exit fullscreen mode

Notice the structure is practically identical to the ReplicaSet’s โ€” which makes sense, since a Deployment creates and manages ReplicaSets behind the scenes. The difference lies in the kind and the additional functionality the Deployment offers.

Checking the creation:

$ kubectl get deploy

Enter fullscreen mode Exit fullscreen mode

Service

Finally, the Service is the object responsible for exposing Pods in a stable way, with a fixed address, regardless of how many times the Pods are recreated.

apiVersion: v1
kind: Service
metadata:
  name: myservice
spec:
  selector:
    app: mypod-label
  ports:
    - port: 80
  type: LoadBalancer  # ClusterIP | NodePort | LoadBalancer

Enter fullscreen mode Exit fullscreen mode

The type field defines how the service will be exposed:

  • ClusterIP (default): exposes the service only internally, within the cluster;
  • NodePort: exposes the service on a fixed port across all cluster nodes (we used this type in Part 5, with Nginx);
  • LoadBalancer: requests an external load balancer โ€” in the cloud, automatically provisioned by the provider; on-premise, it usually requires a manually configured IP or a solution like MetalLB.

Checking the creation:

$ kubectl get services

Enter fullscreen mode Exit fullscreen mode

Summary

Object Main function Pod Minimum unit of execution (one or more containers) ReplicaSet Guarantees a fixed number of replicas of a Pod Deployment Manages ReplicaSets, with support for rolling updates and rollback Service Exposes Pods in a stable way, with a fixed IP and name

Understanding this hierarchy โ€” Pod โ†’ ReplicaSet โ†’ Deployment, plus the Service as the exposure layer โ€” is the foundation for confidently reading (and writing) any Kubernetes manifest, regardless of the application’s complexity.

This wraps up the more conceptual content of this series. In upcoming articles, we’ll keep exploring practical, day-to-day topics of running an on-premise cluster.

Closing the operational loop

With deploying a custom application, controlled updates, rollback, and scalability (manual and automatic) covered, we’ve walked through the most common day-to-day operations of an on-premise Kubernetes cluster. From here, the cluster is ready not just to host applications, but to operate them resiliently as demand grows.

This concludes the more conceptual content of this series. In the upcoming articles, we will continue exploring practical topics related to the day-to-day operations of an on-premise cluster.

In the next articles of the series, we will delve into topics such as private on-premise image registries, persistent storage, and cluster observability.

Related articles:

์›๋ฌธ์—์„œ ๊ณ„์† โ†—

์ฝ”๋ฉ˜ํŠธ

๋‹ต๊ธ€ ๋‚จ๊ธฐ๊ธฐ

์ด๋ฉ”์ผ ์ฃผ์†Œ๋Š” ๊ณต๊ฐœ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ํ•„์ˆ˜ ํ•„๋“œ๋Š” *๋กœ ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค