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:
apiVersionselects the Kubernetes API group and version.kindidentifies the object type, such asPodorDeployment.metadatagives the object identity, including its name and labels.specdescribes the desired state of that object.statusreports 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: v1selects the core API used by Pods.kind: Poddeclares the resource type.metadata.namegives the Pod the stable namefirst-nginx.metadata.namespace: defaultplaces it in the course's ordinary application namespace rather than a system namespace.metadata.labelsattachesapp=first-nginx, which can later select this Pod.spec.containersis a list of containers the Pod should run.imagePullPolicy: IfNotPresentuses the cached image when available.- The named port
httpusescontainerPort: 80andprotocol: 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: 2is the desired number of Pods.selector.matchLabelsidentifies the Pods the Deployment manages.templateis 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.
Inspect the Related Resources
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.


