๐ง๐ท 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 withkubectl 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
LoadBalancertype 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-percentvalues 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.,
v1orapps/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.
-
metadataandspecare 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:
-
createonly creates the object if it doesn’t already exist โ if the object was already created previously, the command returns an error. -
applycreates the object if it doesn’t exist, or updates the existing object if there’s any change in the manifest. Because of this,applyis 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 wideflag 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 nameUnderstanding 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:
๋ต๊ธ ๋จ๊ธฐ๊ธฐ