Learning Kubernetes - Episode 25 - Managing Kubernetes Objects

Learning Kubernetes - Episode 25 - Managing Kubernetes Objects

In this episode, we'll discuss managing Kubernetes objects using imperative and declarative approaches. We'll learn the differences, when to use each method, and best practices for object management.

Arman Dwi Pangestu
Arman Dwi PangestuMarch 31, 2026
0 views
9 min read

Introduction

Note

If you want to read the previous episode, you can click the Episode 24 thumbnail below

Episode 24Episode 24

In the previous episode, we learned about Downward API and how to expose Pod metadata to applications. In episode 25, we'll discuss Managing Kubernetes Objects, exploring both imperative and declarative approaches to creating and managing resources.

Note: Here I'll be using a Kubernetes Cluster installed through K3s.

Understanding how to manage Kubernetes objects is fundamental to working effectively with Kubernetes. There are two main approaches: imperative (telling Kubernetes what to do) and declarative (telling Kubernetes what you want).

What Is Object Management?

Object Management refers to how you create, update, and delete Kubernetes resources. The approach you choose affects maintainability, reproducibility, and collaboration.

Think of object management like cooking - imperative is like following step-by-step instructions ("chop onions, heat oil, add onions"), while declarative is like describing the desired outcome ("I want onion soup"). Both get you to the same place, but the approach differs.

Key characteristics of Object Management:

  • Two approaches - Imperative and declarative
  • Different workflows - Command-line vs configuration files
  • Version control - Declarative enables Git workflows
  • Reproducibility - Declarative ensures consistency
  • Collaboration - Declarative facilitates team work
  • Automation - Declarative enables GitOps

Imperative Management

Imperative management means telling Kubernetes exactly what actions to perform using commands.

Imperative Commands

Create objects directly with kubectl commands.

Creating a Deployment:

Kubernetesbash
sudo kubectl create deployment nginx --image=nginx:1.25

Creating a Service:

Kubernetesbash
sudo kubectl expose deployment nginx --port=80 --type=NodePort

Scaling a Deployment:

Kubernetesbash
sudo kubectl scale deployment nginx --replicas=3

Setting Image:

Kubernetesbash
sudo kubectl set image deployment/nginx nginx=nginx:1.26

Creating a ConfigMap:

Kubernetesbash
sudo kubectl create configmap app-config --from-literal=key1=value1 --from-literal=key2=value2

Creating a Secret:

Kubernetesbash
sudo kubectl create secret generic db-credentials --from-literal=username=admin --from-literal=password=secret123

Imperative Object Configuration

Use configuration files with imperative commands.

Create from file:

Kubernetesbash
sudo kubectl create -f deployment.yml

Replace object:

Kubernetesbash
sudo kubectl replace -f deployment.yml

Delete object:

Kubernetesbash
sudo kubectl delete -f deployment.yml

Advantages of Imperative Management

  • Quick and simple - Fast for one-off tasks
  • Easy to learn - Straightforward commands
  • Interactive - Immediate feedback
  • Good for testing - Rapid experimentation
  • No files needed - Work directly from command line

Disadvantages of Imperative Management

  • Not reproducible - Hard to recreate exact state
  • No version control - Can't track changes in Git
  • Error-prone - Easy to make mistakes
  • Limited collaboration - Hard to share with team
  • No audit trail - Difficult to track who changed what
  • Manual updates - Must remember all commands

Declarative Management

Declarative management means describing the desired state in configuration files and letting Kubernetes figure out how to achieve it.

Declarative Object Configuration

Use kubectl apply with configuration files.

Example: Deployment Configuration

Kubernetesnginx-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
    name: nginx
    labels:
        app: nginx
spec:
    replicas: 3
    selector:
        matchLabels:
            app: nginx
    template:
        metadata:
            labels:
                app: nginx
        spec:
            containers:
                - name: nginx
                  image: nginx:1.25
                  ports:
                      - containerPort: 80

Apply configuration:

Kubernetesbash
sudo kubectl apply -f nginx-deployment.yml

Update configuration (edit file and reapply):

Kubernetesbash
# Edit nginx-deployment.yml (change replicas to 5)
sudo kubectl apply -f nginx-deployment.yml

Apply directory:

Kubernetesbash
sudo kubectl apply -f ./manifests/

Apply recursively:

Kubernetesbash
sudo kubectl apply -f ./manifests/ --recursive

How kubectl apply Works

kubectl apply performs a three-way merge:

  1. Current configuration - What's in the file
  2. Live configuration - What's running in cluster
  3. Last applied configuration - Stored in annotation

This enables intelligent updates that preserve manual changes when possible.

Advantages of Declarative Management

  • Reproducible - Same files produce same results
  • Version control - Track changes in Git
  • Collaboration - Easy to share and review
  • Audit trail - Git history shows all changes
  • Automation - Enable CI/CD and GitOps
  • Self-documenting - Files describe infrastructure
  • Idempotent - Safe to apply multiple times

Disadvantages of Declarative Management

  • More setup - Need to create files
  • Learning curve - Must understand YAML
  • Slower for quick tests - More overhead
  • File management - Need to organize files

Comparing Approaches

Let's compare imperative and declarative management for common tasks.

Creating a Deployment

Imperative:

Kubernetesbash
sudo kubectl create deployment web --image=nginx:1.25 --replicas=3
sudo kubectl expose deployment web --port=80 --type=LoadBalancer

Declarative:

Kubernetesweb-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
    name: web
spec:
    replicas: 3
    selector:
        matchLabels:
            app: web
    template:
        metadata:
            labels:
                app: web
        spec:
            containers:
                - name: nginx
                  image: nginx:1.25
                  ports:
                      - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
    name: web
spec:
    type: LoadBalancer
    selector:
        app: web
    ports:
        - port: 80
          targetPort: 80
Kubernetesbash
sudo kubectl apply -f web-deployment.yml

Scaling a Deployment

Imperative:

Kubernetesbash
sudo kubectl scale deployment web --replicas=5

Declarative:

Kubernetesweb-deployment.yml
# Edit file: change replicas from 3 to 5
spec:
    replicas: 5
Kubernetesbash
sudo kubectl apply -f web-deployment.yml

Updating an Image

Imperative:

Kubernetesbash
sudo kubectl set image deployment/web nginx=nginx:1.26

Declarative:

Kubernetesweb-deployment.yml
# Edit file: change image version
containers:
    - name: nginx
      image: nginx:1.26
Kubernetesbash
sudo kubectl apply -f web-deployment.yml

Best Practices

Use Declarative for Production

Always use declarative management for production environments:

Kubernetesbash
# Good: Declarative
sudo kubectl apply -f production/
 
# Avoid: Imperative in production
sudo kubectl create deployment ...

Use Imperative for Quick Tests

Imperative is fine for development and testing:

Kubernetesbash
# Quick test
sudo kubectl run test-pod --image=nginx:1.25 --rm -it -- /bin/bash

Organize Configuration Files

Structure your manifests logically:

plaintext
manifests/
├── base/
│   ├── deployment.yml
│   ├── service.yml
│   └── configmap.yml
├── dev/
│   └── kustomization.yml
├── staging/
│   └── kustomization.yml
└── production/
    └── kustomization.yml

Use Version Control

Always commit configuration files to Git:

Kubernetesbash
git add manifests/
git commit -m "Add nginx deployment"
git push

Add Labels and Annotations

Include metadata for organization:

Kubernetesyml
metadata:
    name: nginx
    labels:
        app: nginx
        version: v1.0
        environment: production
    annotations:
        description: "Production web server"
        owner: "platform-team"

Use Namespaces

Organize resources by namespace:

Kubernetesyml
metadata:
    name: nginx
    namespace: production

Document Your Configurations

Add comments to explain complex configurations:

Kubernetesyml
# Nginx deployment for production web traffic
# Configured with 3 replicas for high availability
apiVersion: apps/v1
kind: Deployment
metadata:
    name: nginx

Practical Examples

Example 1: Complete Application Stack

Declarative approach:

Kubernetesapp-stack.yml
# ConfigMap for application configuration
apiVersion: v1
kind: ConfigMap
metadata:
    name: app-config
data:
    DATABASE_HOST: "postgres"
    DATABASE_PORT: "5432"
    LOG_LEVEL: "info"
---
# Secret for sensitive data
apiVersion: v1
kind: Secret
metadata:
    name: app-secrets
type: Opaque
stringData:
    DATABASE_PASSWORD: "secretpassword"
    API_KEY: "abc123xyz789"
---
# Deployment for application
apiVersion: apps/v1
kind: Deployment
metadata:
    name: app
    labels:
        app: myapp
spec:
    replicas: 3
    selector:
        matchLabels:
            app: myapp
    template:
        metadata:
            labels:
                app: myapp
        spec:
            containers:
                - name: app
                  image: myapp:latest
                  ports:
                      - containerPort: 8080
                  envFrom:
                      - configMapRef:
                            name: app-config
                      - secretRef:
                            name: app-secrets
---
# Service to expose application
apiVersion: v1
kind: Service
metadata:
    name: app
spec:
    type: LoadBalancer
    selector:
        app: myapp
    ports:
        - port: 80
          targetPort: 8080

Apply everything:

Kubernetesbash
sudo kubectl apply -f app-stack.yml

Example 2: Multi-Environment Configuration

Base configuration:

Kubernetesbase/deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
    name: app
spec:
    replicas: 1
    selector:
        matchLabels:
            app: myapp
    template:
        metadata:
            labels:
                app: myapp
        spec:
            containers:
                - name: app
                  image: myapp:latest
                  ports:
                      - containerPort: 8080

Development overlay:

Kubernetesdev/deployment-patch.yml
apiVersion: apps/v1
kind: Deployment
metadata:
    name: app
spec:
    replicas: 1
    template:
        spec:
            containers:
                - name: app
                  env:
                      - name: ENVIRONMENT
                        value: "development"

Production overlay:

Kubernetesprod/deployment-patch.yml
apiVersion: apps/v1
kind: Deployment
metadata:
    name: app
spec:
    replicas: 5
    template:
        spec:
            containers:
                - name: app
                  env:
                      - name: ENVIRONMENT
                        value: "production"
                  resources:
                      requests:
                          memory: "256Mi"
                          cpu: "250m"
                      limits:
                          memory: "512Mi"
                          cpu: "500m"

Example 3: GitOps Workflow

Directory structure:

plaintext
kubernetes/
├── apps/
│   ├── frontend/
│   │   ├── deployment.yml
│   │   ├── service.yml
│   │   └── ingress.yml
│   └── backend/
│       ├── deployment.yml
│       ├── service.yml
│       └── configmap.yml
└── infrastructure/
    ├── namespaces.yml
    └── rbac.yml

Apply with Git workflow:

Kubernetesbash
# Clone repository
git clone https://github.com/company/kubernetes-manifests.git
cd kubernetes-manifests
 
# Apply infrastructure
sudo kubectl apply -f infrastructure/
 
# Apply applications
sudo kubectl apply -f apps/
 
# Make changes
vim apps/frontend/deployment.yml
 
# Commit and push
git add apps/frontend/deployment.yml
git commit -m "Update frontend to v2.0"
git push
 
# Apply changes
sudo kubectl apply -f apps/frontend/

kubectl apply vs kubectl create

Understanding the differences:

kubectl create

  • Creates new resources only
  • Fails if resource already exists
  • Imperative approach
  • No three-way merge
Kubernetesbash
# Creates new deployment
sudo kubectl create -f deployment.yml
 
# Fails if deployment exists
# Error: deployments.apps "nginx" already exists

kubectl apply

  • Creates or updates resources
  • Idempotent (safe to run multiple times)
  • Declarative approach
  • Performs three-way merge
  • Stores last-applied-configuration
Kubernetesbash
# Creates deployment if not exists
sudo kubectl apply -f deployment.yml
 
# Updates deployment if exists
sudo kubectl apply -f deployment.yml

kubectl replace vs kubectl apply

kubectl replace

  • Replaces entire resource
  • Requires complete configuration
  • Removes fields not in new config
  • Fails if resource doesn't exist
Kubernetesbash
sudo kubectl replace -f deployment.yml

kubectl apply

  • Merges changes intelligently
  • Preserves unspecified fields
  • Creates if doesn't exist
  • Preferred for declarative management
Kubernetesbash
sudo kubectl apply -f deployment.yml

Dry Run and Diff

Test changes before applying.

Dry Run

Preview what would happen:

Tip

Dry run client side only does YAML file parsing and validation, it does not perform validation to the API Server.


Whereas server side will perform full lifecycle validation up to the API Server without actually committing so it is not applied to etcd. Here are the differences:


FeatureClientServer
Validasi schemalocalserver
Admission controller
Default value dari server
Real behavior simulation
Kubernetesbash
# Client-side dry run
sudo kubectl apply -f deployment.yml --dry-run=client
 
# Server-side dry run
sudo kubectl apply -f deployment.yml --dry-run=server

Example result of dry run client and server side

apiVersion: v1
items:
- apiVersion: apps/v1
  kind: Deployment
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"name":"web","namespace":"default"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"web"}},"template":{"metadata":{"labels":{"app":"web"}},"spec":{"containers":[{"image":"nginx:1.25","name":"nginx","ports":[{"containerPort":80}]}]}}}}
    name: web
    namespace: default
  spec:
    replicas: 3
    selector:
      matchLabels:
        app: web
    template:
      metadata:
        labels:
          app: web
      spec:
        containers:
        - image: nginx:1.25
          name: nginx
          ports:
          - containerPort: 80
- apiVersion: v1
  kind: Service
  metadata:
    annotations:
      kubectl.kubernetes.io/last-applied-configuration: |
        {"apiVersion":"v1","kind":"Service","metadata":{"annotations":{},"name":"web","namespace":"default"},"spec":{"ports":[{"port":80,"targetPort":80}],"selector":{"app":"web"},"type":"LoadBalancer"}}
    name: web
    namespace: default
  spec:
    ports:
    - port: 80
      targetPort: 80
    selector:
      app: web
    type: LoadBalancer
kind: List
metadata: {}

Diff

See differences before applying:

Kubernetesbash
sudo kubectl diff -f deployment.yml

Output shows what would change:

Kubernetesyml
--- /tmp/LIVE-2656508399/apps.v1.Deployment.default.web
+++ /tmp/MERGED-975783886/apps.v1.Deployment.default.web
@@ -6,14 +6,14 @@
     kubectl.kubernetes.io/last-applied-configuration: |
       {"apiVersion":"apps/v1","kind":"Deployment","metadata":{"annotations":{},"name":"web","namespace":"default"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"web"}},"template":{"metadata":{"labels":{"app":"web"}},"spec":{"containers":[{"image":"nginx:1.25","name":"nginx","ports":[{"containerPort":80}]}]}}}}
   creationTimestamp: "2026-03-26T16:55:41Z"
-  generation: 1
+  generation: 2
   name: web
   namespace: default
   resourceVersion: "480086"
   uid: 69e521b0-5556-4354-a788-db0463e1a935
 spec:
   progressDeadlineSeconds: 600
-  replicas: 3
+  replicas: 5
   revisionHistoryLimit: 10
   selector:
     matchLabels:

Common Mistakes and Pitfalls

Mistake 1: Mixing Imperative and Declarative

Problem: Using both approaches causes confusion.

Solution: Choose one approach per environment:

Kubernetesbash
# Bad: Mixing approaches
sudo kubectl create deployment nginx --image=nginx:1.25
sudo kubectl apply -f service.yml
 
# Good: Consistent approach
sudo kubectl apply -f deployment.yml
sudo kubectl apply -f service.yml

Mistake 2: Not Using Version Control

Problem: No history of changes.

Solution: Always commit configurations:

Kubernetesbash
git add manifests/
git commit -m "Update deployment"

Mistake 3: Imperative Changes in Production

Problem: Manual changes not tracked.

Solution: Always update files and apply:

Kubernetesbash
# Bad: Direct change
sudo kubectl scale deployment nginx --replicas=5
 
# Good: Update file and apply
# Edit deployment.yml
sudo kubectl apply -f deployment.yml

Mistake 4: Not Testing Changes

Problem: Applying untested configurations.

Solution: Use dry-run and diff:

Kubernetesbash
sudo kubectl diff -f deployment.yml
sudo kubectl apply -f deployment.yml --dry-run=server

Mistake 5: Incomplete Configurations

Problem: Missing required fields.

Solution: Use complete, valid YAML:

Kubernetesyml
# Bad: Incomplete
apiVersion: apps/v1
kind: Deployment
metadata:
    name: nginx
 
# Good: Complete
apiVersion: apps/v1
kind: Deployment
metadata:
    name: nginx
spec:
    replicas: 3
    selector:
        matchLabels:
            app: nginx
    template:
        metadata:
            labels:
                app: nginx
        spec:
            containers:
                - name: nginx
                  image: nginx:1.25

When to Use Each Approach

Use Imperative When:

  • Quick testing and experimentation
  • Learning Kubernetes
  • One-off debugging tasks
  • Development environment
  • Interactive troubleshooting
Kubernetesbash
# Quick test
sudo kubectl run test --image=nginx:1.25 --rm -it -- /bin/bash

Use Declarative When:

  • Production environments
  • Team collaboration
  • CI/CD pipelines
  • GitOps workflows
  • Infrastructure as Code
  • Reproducible deployments
Kubernetesbash
# Production deployment
sudo kubectl apply -f production/

Viewing and Managing Objects

List Objects

Kubernetesbash
sudo kubectl get deployments
sudo kubectl get pods
sudo kubectl get services

Describe Objects

Kubernetesbash
sudo kubectl describe deployment nginx

View YAML

Kubernetesbash
sudo kubectl get deployment nginx -o yaml

Edit Objects

Kubernetesbash
# Opens editor (not recommended for production)
sudo kubectl edit deployment nginx

Delete Objects

Kubernetesbash
# Imperative
sudo kubectl delete deployment nginx
 
# Declarative
sudo kubectl delete -f deployment.yml

Conclusion

In episode 25, we've explored Managing Kubernetes Objects using both imperative and declarative approaches. We've learned the differences, advantages, and when to use each method.

Key takeaways:

  • Two approaches - Imperative (commands) and declarative (files)
  • Imperative tells Kubernetes what to do step-by-step
  • Declarative describes desired state in configuration files
  • Use kubectl create/scale/set for imperative management
  • Use kubectl apply for declarative management
  • Declarative is preferred for production environments
  • Version control enables collaboration and audit trails
  • kubectl apply performs intelligent three-way merge
  • Dry run and diff help test changes before applying
  • Organize files in logical directory structure
  • GitOps enables automated deployments from Git
  • Imperative is fine for quick tests and learning
  • Don't mix approaches in the same environment
  • Always test with dry-run before applying
  • Document configurations with comments and annotations

Understanding object management is fundamental to working effectively with Kubernetes. By choosing the right approach for your use case, you can build maintainable, reproducible, and collaborative infrastructure.

Are you getting a clearer understanding of Managing Kubernetes Objects? Keep your learning momentum going and look forward to the next episode!

Note

If you want to continue to the next episode, you can click the Episode 26 thumbnail below

Episode 26Episode 26

Related Posts