Bash and kubectl are your Swiss Army knife for automating Kubernetes tasks, but their real power isn’t just running commands — it’s about orchestrating complex workflows that would otherwise be manual, error-prone, and tedious.

Let’s see this in action. Imagine you need to deploy a new version of an application, but before you do, you want to ensure the current version is healthy, back up its persistent volume claims, and then roll out the new version with a controlled canary deployment.

Here’s a Bash script that does just that:

#!/bin/bash

NAMESPACE="default"
DEPLOYMENT_NAME="my-app"
CONTAINER_IMAGE="my-registry/my-app:v1.1.0"
CANARY_PERCENTAGE=10 # 10% of traffic to the new version

echo "--- Checking current deployment health ---"
kubectl get pods -n $NAMESPACE -l app=$DEPLOYMENT_NAME --field-selector=status.phase!=Running,status.phase!=Succeeded,status.phase!=Completed
if [ $? -ne 0 ]; then
  echo "ERROR: Some pods are not in a healthy state. Aborting."
  exit 1
fi
echo "Current deployment is healthy."

echo "--- Backing up Persistent Volume Claims ---"
PVC_NAMES=$(kubectl get pvc -n $NAMESPACE -l app=$DEPLOYMENT_NAME -o jsonpath='{.items[*].metadata.name}')
if [ -n "$PVC_NAMES" ]; then
  for pvc in $PVC_NAMES; do
    echo "Backing up PVC: $pvc"
    # In a real scenario, this would involve snapshotting or copying data.
    # For demonstration, we'll just create a placeholder.
    mkdir -p ./pvc_backups/$pvc
    echo "Backup placeholder for $pvc created."
  done
else
  echo "No PVCs found for $DEPLOYMENT_NAME. Skipping backup."
fi

echo "--- Creating new deployment with canary ---"

# Scale down the original deployment to zero replicas temporarily
kubectl scale deployment $DEPLOYMENT_NAME --replicas=0 -n $NAMESPACE

# Create a new deployment for the canary version
cat <<EOF | kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${DEPLOYMENT_NAME}-canary
  namespace: $NAMESPACE
  labels:
    app: $DEPLOYMENT_NAME
spec:
  replicas: 1
  selector:
    matchLabels:
      app: $DEPLOYMENT_NAME
      version: canary
  template:
    metadata:
      labels:
        app: $DEPLOYMENT_NAME
        version: canary
    spec:
      containers:
      - name: $DEPLOYMENT_NAME
        image: $CONTAINER_IMAGE
        ports:
        - containerPort: 80
EOF

echo "Canary deployment created. Waiting for it to become ready..."
kubectl rollout status deployment/${DEPLOYMENT_NAME}-canary -n $NAMESPACE --timeout=300s

echo "--- Configuring Service for Canary ---"
# Assuming a Service named 'my-app-service' routes traffic to the app
SERVICE_NAME="my-app-service"

# Get the current service selector
CURRENT_SELECTOR=$(kubectl get service $SERVICE_NAME -n $NAMESPACE -o jsonpath='{.spec.selector}')

# Create a new service or update the existing one to route traffic
# This is a simplified example. In reality, you'd likely use Istio, Linkerd, or a weighted target group.
# For basic kubectl, we'd manipulate the service selector or create a new one.
# Here, we'll demonstrate a common pattern of updating the service to point to a new version via labels.

# Label the existing pods to direct traffic to the canary
# This is a VERY basic approach and not robust for production canarying.
# A real canary involves traffic splitting via a service mesh or ingress controller.
echo "Updating labels on existing pods (for demonstration of traffic shift logic)..."
# This command would typically be part of a traffic management solution, not direct pod labeling for traffic.
# A proper solution would involve updating the Service's selector or using an Ingress/Service Mesh.

# For a true canary, you'd use a Service Mesh (like Istio) or Ingress Controller.
# Example using Istio (conceptual, requires Istio installed):
# kubectl apply -f - <<EOF
# apiVersion: networking.istio.io/v1alpha3
# kind: VirtualService
# metadata:
#   name: $SERVICE_NAME
#   namespace: $NAMESPACE
# spec:
#   hosts:
#   - "*"
#   http:
#   - route:
#     - destination:
#         host: $SERVICE_NAME
#         subset: current # Assuming a 'current' subset for the stable version
#       weight: $((100 - CANARY_PERCENTAGE))
#     - destination:
#         host: $SERVICE_NAME
#         subset: canary # Assuming a 'canary' subset for the new version
#       weight: $CANARY_PERCENTAGE
# ---
# apiVersion: networking.istio.io/v1alpha3
# kind: DestinationRule
# metadata:
#   name: $SERVICE_NAME
#   namespace: $NAMESPACE
# spec:
#   host: $SERVICE_NAME
#   subsets:
#   - name: current
#     labels:
#       app: $DEPLOYMENT_NAME
#       version: stable
#   - name: canary
#     labels:
#       app: $DEPLOYMENT_NAME
#       version: canary
# EOF

echo "Canary traffic configured (conceptually). Monitor your application."

echo "--- Deployment process complete. Monitor your canary ---"
echo "If canary is stable, you would then gradually shift all traffic and update the main deployment."

This script tackles a common operational challenge: safely rolling out new application versions.

The core problem it solves is that deploying new code directly to all instances at once is risky. If there’s a bug, your entire application can go down. This script introduces a phased rollout, starting with a small percentage of traffic (CANARY_PERCENTAGE=10) directed to the new version, allowing you to observe its behavior before committing to a full rollout.

Here’s how it works internally:

  1. Health Check: It first queries for pods that are not in a Running, Succeeded, or Completed state. If any such pods exist, it halts the process, preventing a potentially bad deployment from proceeding.
  2. Data Backup: It identifies any PersistentVolumeClaims (PVCs) associated with the deployment and creates a placeholder backup directory. In a real-world scenario, this step would involve invoking volume snapshotting tools or data migration scripts.
  3. Canary Deployment Creation: It scales the existing deployment to zero replicas, ensuring no traffic hits the old version during the setup. Then, it applies a new deployment manifest (my-app-canary) that specifies the new container image and a distinct label (version: canary). This creates a separate set of pods running the new code.
  4. Rollout Status: It waits for the canary deployment to become ready, confirming that the new pods are up and running.
  5. Traffic Management (Conceptual): The most complex part is directing traffic. The script demonstrates the idea by showing how you might label existing pods or conceptually use a service mesh like Istio. In a production environment, you’d typically integrate with an Ingress controller (like Nginx or Traefik) or a service mesh (like Istio or Linkerd) that supports weighted traffic splitting. These tools allow you to configure rules to send a percentage of requests to the canary deployment and the rest to the stable version. The provided Bash script uses comments to illustrate what an Istio configuration would look like.

The kubectl commands are the building blocks: get pods, scale deployment, apply -f - (to apply a manifest from standard input), and rollout status. Bash scripting provides the orchestration: conditional logic (if [ $? -ne 0 ]), loops (for pvc in $PVC_NAMES), and here-documents (cat <<EOF | kubectl apply -f -) to dynamically create Kubernetes manifests.

The "surprise" here is that kubectl itself doesn’t have built-in, robust canary traffic splitting. You can manipulate Service selectors or Pod labels, but it’s clunky and error-prone for real-world traffic management. The actual sophisticated canarying is handled by other components (Ingress, Service Mesh) that kubectl then configures. The Bash script acts as the conductor, telling these other components what to do.

If you successfully deploy your canary and it’s stable, the next logical step is to gradually shift the remaining traffic, update the primary deployment to the new version, and then remove the canary deployment.

Want structured learning?

Take the full Bash course →