Deploy Applications on Kubernetes

KubernetesBeginner
Practice Now

Introduction

In the first course section, you explored a Kubernetes cluster without changing it. You learned that kubectl sends requests to the API server and that Kubernetes controllers continually compare desired state with actual state.

Now you will make your first application requests. Instead of telling Kubernetes every low-level action to perform, you will describe the result you want in YAML files called manifests. Kubernetes stores those object definitions and works to realize them.

You will begin with a single Pod so that the basic manifest structure is easy to see. You will then define a Deployment that manages two Pods. Comparing the bare Pod with Deployment-managed Pods will show why higher-level controllers are normally preferred for applications.

This lab deliberately focuses on workload creation. You will learn how to expose applications through Services later, after Pods, labels, and Deployments are familiar.

Understand Declarative Kubernetes Objects

In this step, you will connect the desired-state idea from the previous lab to Kubernetes manifests and prepare a workspace for your first application definitions.

Environment startup: This lab starts a complete Kubernetes cluster for you. Configuring its control plane, node, and networking components usually takes 2–3 minutes. Please wait patiently for the environment to finish loading before running the commands below.

From Commands to Desired State

Kubernetes supports two broad management styles:

  • With an imperative command, you directly request an action, such as “create a Pod named first-nginx.”
  • With a declarative manifest, you save the desired object configuration in a file and ask Kubernetes to make the cluster match it.

Declarative files are valuable because you can read them before making a change, apply them repeatedly, review differences, and store them in version control. This course will emphasize the declarative style.

Every Kubernetes object returned by the API has important top-level fields:

  • apiVersion selects the Kubernetes API group and version.
  • kind identifies the object type, such as Pod or Deployment.
  • metadata gives the object identity, including its name and labels.
  • spec describes the desired state of that object.
  • status reports observed state and is normally filled in by Kubernetes after creation, not written in your manifest.

This creates an important loop:

manifest spec -> API server stores desired state -> controllers act -> object status reports actual state

Prepare the Manifest Directory

Move to the directory prepared for this lab:

cd /home/labex/project/k8s-manifests

Confirm your location with pwd, which means print working directory:

pwd
/home/labex/project/k8s-manifests

Create a short notes file recording the four manifest fields you will use. The printf command writes each quoted string as a separate line, and > redirects that output into a file, replacing the file if it already exists.

printf '%s\n' apiVersion kind metadata spec > manifest-fields.txt

Display the file to verify it:

cat manifest-fields.txt
apiVersion
kind
metadata
spec

The notes file is a small learning checkpoint: these four fields will appear in both manifests you create next.

Write and Validate a Pod Manifest

In this step, you will write a YAML manifest for one Pod and validate its structure before sending it to the cluster.

Meet the Pod

A Pod is the smallest deployable Kubernetes object. A Pod gives one or more tightly related containers a shared network identity and storage context. Most beginner examples use one container per Pod.

The Pod in this lab will run NGINX, a small web server. The image is pinned to nginx:1.27-alpine. Pinning a version makes the result more reproducible than using the moving latest tag. The image has already been cached in the cluster so learner work does not depend on an internet download.

Create the YAML File

Make sure you are in the manifest directory:

cd /home/labex/project/k8s-manifests

You will use a here-document to create the file. The shell redirects every line between <<'EOF' and the closing EOF into first-pod.yaml. Quoting the first EOF prevents the shell from expanding special characters inside the YAML.

cat <<'EOF' > first-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: first-nginx
  namespace: default
  labels:
    app: first-nginx
spec:
  containers:
    - name: nginx
      image: nginx:1.27-alpine
      imagePullPolicy: IfNotPresent
      ports:
        - name: http
          containerPort: 80
          protocol: TCP
EOF

YAML represents hierarchy with indentation. Use spaces consistently; tabs can make YAML invalid. A hyphen, as in - name: nginx, begins an item in a list.

Read the object from top to bottom:

  • apiVersion: v1 selects the core API used by Pods.
  • kind: Pod declares the resource type.
  • metadata.name gives the Pod the stable name first-nginx.
  • metadata.namespace: default places it in the course's ordinary application namespace rather than a system namespace.
  • metadata.labels attaches app=first-nginx, which can later select this Pod.
  • spec.containers is a list of containers the Pod should run.
  • imagePullPolicy: IfNotPresent uses the cached image when available.
  • The named port http uses containerPort: 80 and protocol: TCP. It documents where NGINX listens inside the container; it does not expose the Pod outside the cluster.

Validate Before Creating

Use a client-side dry run to parse the file without creating the Pod. The -f option means file, and --dry-run=client keeps the request local:

kubectl apply --dry-run=client -f first-pod.yaml
pod/first-nginx created (dry run)

The words dry run are essential: the syntax is valid, but the cluster has not been changed.

Ask kubectl to print the normalized object as YAML:

The output option -o means output format. Supplying yaml asks kubectl to render the parsed object as YAML instead of printing only a one-line result:

kubectl apply --dry-run=client -f first-pod.yaml -o yaml

You will see your fields plus defaults added by the client. This is useful for catching indentation, field-name, and type mistakes before a real apply.

Create and Inspect Your First Pod

In this step, you will apply the validated manifest, watch Kubernetes move the Pod toward its desired state, and inspect the resulting object.

Apply the Manifest

Move to the manifest directory if necessary:

cd /home/labex/project/k8s-manifests

Apply the file without the dry-run option:

kubectl apply -f first-pod.yaml
pod/first-nginx created

kubectl apply sends the object to the API server. The API server stores the desired Pod specification, and the scheduler and kubelet cooperate to run it on the node.

Wait for Readiness

Pod creation is asynchronous: kubectl apply can return before the container is ready. Use kubectl wait to wait for the Pod's Ready condition. The command stops successfully when the condition becomes true or fails after 60 seconds:

kubectl wait --for=condition=Ready pod/first-nginx --timeout=60s
pod/first-nginx condition met

Now list the Pod. This lab repeats -o wide because each lab should be usable on its own: -o selects an output format, and wide adds fields such as Pod IP and node name:

kubectl get pod first-nginx -o wide
NAME          READY   STATUS    RESTARTS   AGE   IP           NODE
first-nginx   1/1     Running   ...        ...   ...          labex-v135

READY=1/1 means the one container is ready, while STATUS=Running is the Pod phase. The wide view also shows the Pod IP and assigned node. Pod IPs and ages are generated values, so they can differ.

Inspect Labels and Ownership

Display the Pod's labels:

kubectl get pod first-nginx --show-labels

Look for app=first-nginx. Labels are stored with the object and will become important when Deployments and Services select Pods.

Ask Kubernetes whether another object controls this Pod. -o jsonpath='...' extracts selected fields instead of printing the whole object. The expression walks through metadata.ownerReferences; {"\n"} adds a final newline so the shell prompt appears on the next line:

kubectl get pod first-nginx -o jsonpath='Owner: {.metadata.ownerReferences[0].kind}{"\n"}'
Owner:

The owner is blank because you created this bare Pod directly. If it is deleted, no higher-level controller knows that it should be replaced. You will compare this with managed Pods in the next steps.

Step Checkpoint

You turned a local desired-state file into a running Kubernetes object. The API accepted the Pod, the scheduler assigned it, and the kubelet made its container ready. The Pod exists, but no controller manages its lifecycle.

Define a Deployment

In this step, you will define a Deployment that asks Kubernetes to maintain two copies of an NGINX Pod.

Why Use a Deployment?

A bare Pod is useful for learning, but applications usually need a controller. A Deployment provides a desired replica count and Pod template. It creates a ReplicaSet, and the ReplicaSet maintains the requested Pods.

The ownership chain is:

Deployment -> ReplicaSet -> Pods -> containers

If a managed Pod disappears, the ReplicaSet notices that actual replicas are below desired replicas and creates a replacement. Later labs will use Deployments for scaling and rolling updates.

Create the Deployment Manifest

Return to the manifest directory:

cd /home/labex/project/k8s-manifests

Create course-web-deployment.yaml with a here-document:

cat <<'EOF' > course-web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: course-web
  labels:
    app: course-web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: course-web
  template:
    metadata:
      labels:
        app: course-web
    spec:
      containers:
        - name: nginx
          image: nginx:1.27-alpine
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 80
EOF

The Deployment uses apps/v1, the stable API for Deployments. Its spec introduces three important fields:

  • replicas: 2 is the desired number of Pods.
  • selector.matchLabels identifies the Pods the Deployment manages.
  • template is the blueprint used to create each Pod.

The selector and template.metadata.labels both use app: course-web. They must match; otherwise the Deployment could not identify Pods created from its own template.

Validate the Deployment

Parse the manifest without changing the cluster:

kubectl apply --dry-run=client -f course-web-deployment.yaml
deployment.apps/course-web created (dry run)

Use kubectl diff to compare the manifest with live state. A nonzero diff exit simply means the object would change; || true keeps the shell prompt from treating that expected difference as a failure:

kubectl diff -f course-web-deployment.yaml || true

Because course-web does not exist yet, the output shows the complete object as an addition, with lines beginning with +. Unlike apply, diff makes no cluster change.

Deploy and Inspect the Managed Application

In this step, you will apply the Deployment, wait for its two replicas, and inspect the related resources selected by their shared label.

Apply and Wait for the Deployment

First change to the directory containing the manifest. Then apply the saved desired state; -f tells kubectl to read from that file:

cd /home/labex/project/k8s-manifests
kubectl apply -f course-web-deployment.yaml
deployment.apps/course-web created

Wait for the Deployment rollout to complete. A rollout is the process of bringing the Deployment's Pods to the desired template and replica count:

kubectl rollout status deployment/course-web --timeout=60s
deployment "course-web" successfully rolled out

List the Deployment:

kubectl get deployment course-web
NAME         READY   UP-TO-DATE   AVAILABLE   AGE
course-web   2/2     2            2           ...

READY=2/2 means both desired replicas are ready. UP-TO-DATE=2 means both use the current Pod template, and AVAILABLE=2 means both are available.

Use the label selector -l app=course-web to list related resources:

kubectl get deployment,replicaset,pods -l app=course-web

The output contains one Deployment, one ReplicaSet, and two Pods. Generated ReplicaSet and Pod suffixes will vary:

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

NAME                                    DESIRED   CURRENT   READY   AGE
replicaset.apps/course-web-...          2         2         2       ...

NAME                              READY   STATUS    RESTARTS   AGE
pod/course-web-...-...            1/1     Running   ...        ...
pod/course-web-...-...            1/1     Running   ...        ...

This view proves that the Deployment's desired two replicas have become two ready Pods. You will trace the ownership connections between these resources in the next step.

Step Checkpoint

You applied a Deployment manifest and waited for its desired state. Kubernetes created a ReplicaSet and two Pods, and the shared app=course-web label allowed you to list them as one application group.

Trace Controller Ownership

In this step, you will follow Kubernetes owner references from a managed Pod to its ReplicaSet and then to the Deployment. You will also reapply the manifest to observe declarative idempotence.

Inspect a Pod Owner

Store one generated Pod name in a shell variable. The syntax NAME=$(command) is command substitution: the shell runs the command and saves its output in NAME. Here, -l app=course-web selects matching Pods and JSONPath extracts the first Pod's generated name:

POD_NAME=$(kubectl get pods -l app=course-web -o jsonpath='{.items[0].metadata.name}')

Print it so you know which Pod was selected:

echo "$POD_NAME"

Now inspect its direct owner. Quoting "$POD_NAME" passes the stored name as one safe command argument:

kubectl get pod "$POD_NAME" -o jsonpath='Owner: {.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
Owner: ReplicaSet/course-web-...

Unlike the bare first-nginx Pod, a Deployment-managed Pod has a ReplicaSet owner. The ReplicaSet itself is owned by the Deployment.

Display the ReplicaSet owner. The first command repeats the same command-substitution pattern, this time storing a ReplicaSet name in RS_NAME:

RS_NAME=$(kubectl get replicaset -l app=course-web -o jsonpath='{.items[0].metadata.name}')
kubectl get replicaset "$RS_NAME" -o jsonpath='Owner: {.metadata.ownerReferences[0].kind}/{.metadata.ownerReferences[0].name}{"\n"}'
Owner: Deployment/course-web

Reapply the Desired State

Apply the same manifest again:

kubectl apply -f course-web-deployment.yaml
deployment.apps/course-web unchanged

unchanged demonstrates an important declarative property: applying the same desired state repeatedly is safe. Kubernetes only needs to act when desired and actual configuration differ.

Step Checkpoint

You now have a bare Pod and a Deployment-managed application. Both run containers, but the Deployment adds a controller hierarchy that maintains two replicas and provides a foundation for future scaling and rolling updates.

Summary

You moved from read-only cluster exploration to declarative application management. You learned the roles of apiVersion, kind, metadata, and spec; validated manifests with client-side dry runs; created and inspected a bare Pod; and deployed two managed replicas with a Deployment.

Most importantly, you observed the controller ownership chain from Deployment to ReplicaSet to Pods. This desired-state foundation prepares you to diagnose applications, expose them through Services, scale replicas, and perform rolling updates in later course sections.