Deploy Containers on EKS with kubectl: YAML, Logs, and Rollbacks
By the end of this guide you will be able to deploy a containerised application to Amazon EKS using kubectl, verify the rollout succeeded, read logs from a running pod, reach it locally with port-forwarding, and roll back safely if the new version causes problems. This is the full kubectl workflow for EKS: not just a list of commands, but a deployment you can follow end to end.
All examples use a consistent app called demo-app, the production namespace, region eu-west-1, and account ID 123456789012. Swap these for your own values.
Simple explanation
kubectl is a command-line tool that talks to your Kubernetes cluster’s API server. Every action you take (deploying an app, scaling it, reading logs, deleting a pod) is an HTTP request to that API server.
On EKS, AWS runs the control plane (the API server, scheduler, and controllers) so you do not have to manage it. You point kubectl at the cluster and start giving it instructions. When you apply a Deployment manifest, the API server stores your desired state, the scheduler finds nodes with capacity, and the Deployment controller creates pods on those nodes. Each pod pulls your container image, starts the process, and begins accepting traffic once it passes its readiness probe.
The key difference from docker run is that you declare what you want, not how to do it. You say “I want 3 replicas of this container running.” If one crashes, Kubernetes restarts it. If a node fails, Kubernetes reschedules the affected pods.
Think of kubectl as a remote control and EKS as the appliance. You press a button (apply a manifest) and the appliance figures out what to do internally. You do not need to know which internal components moved. You describe the outcome you want and check that it happened.
Before you start
You will need all of the following before the commands in this guide will work:
- An EKS cluster already running. See Creating your first EKS cluster if you have not done this yet.
- kubectl installed (
kubectl version --clientshould return a version) - AWS CLI installed and configured (
aws sts get-caller-identityshould return your account) - kubeconfig updated for the cluster (covered in step 1 below)
- A container image available in Amazon ECR or another accessible registry
- Optionally: a namespace already chosen for your workload
How it works on EKS
When you run kubectl apply, here is what actually happens:
- kubectl reads your YAML and sends an HTTP request to the EKS API server
- The API server validates the manifest and stores the desired state in etcd
- The Deployment controller notices the new desired state and creates a ReplicaSet
- The scheduler assigns each pod to a node based on resource availability
- The kubelet on each node pulls the container image from ECR (or your registry) and starts it
- Once the pod passes its readiness probe, the Service starts routing traffic to it
The EKS control plane handles steps 2 through 4 for you. You interact only with the API server via kubectl.
Think of kubectl as a remote control and EKS as the appliance it controls. You press a button (apply a manifest) and the appliance figures out what to do internally. You do not need to know which internal components moved. You describe the outcome you want and check that it happened.
Step-by-step: deploy a container to EKS with kubectl
Step 1: Configure kubectl for the cluster
Before running any kubectl commands, point kubectl at your EKS cluster:
aws eks update-kubeconfig \
--region eu-west-1 \
--name my-clusterThis writes an entry to ~/.kube/config and sets it as the active context. Verify it worked:
# Show the active context
kubectl config current-context
# Expected: arn:aws:eks:eu-west-1:123456789012:cluster/my-cluster
# List all configured contexts
kubectl config get-contexts
# Switch to a different context
kubectl config use-context arn:aws:eks:eu-west-1:123456789012:cluster/my-clusterAlways run kubectl config current-context before destructive commands. Running kubectl delete deployment against the wrong cluster is a common and painful mistake. Some teams use a terminal plugin that changes the prompt colour based on the active context so the risk is always visible.
Step 2: Create or choose a namespace
Namespaces separate workloads inside the same cluster. Keeping production and staging in different namespaces on the same cluster is a common pattern.
Create a namespace with a manifest (recommended for anything tracked in version control):
# namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: productionkubectl apply -f namespace.yamlOr create it directly:
kubectl create namespace productionEvery command in this guide uses -n production. Omitting the flag defaults to the default namespace, which is why pods “disappear” after forgetting to include it.
If you switch between clusters and namespaces frequently, install kubectx and kubens. They let you switch active context and namespace with a single short command instead of typing the full kubectl config command each time.
Step 3: Create the Deployment manifest
A Deployment is the standard way to run a containerised application on Kubernetes. It manages the desired replica count, handles rolling updates, and restarts failed pods automatically.
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-app
namespace: production
labels:
app: demo-app
spec:
replicas: 3
selector:
matchLabels:
app: demo-app
template:
metadata:
labels:
app: demo-app
spec:
containers:
- name: demo-app
image: 123456789012.dkr.ecr.eu-west-1.amazonaws.com/demo-app:1.2.0
ports:
- containerPort: 8080
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256MiA few things worth noting:
- The
selector.matchLabelsmust match thetemplate.metadata.labels. This is how the Deployment knows which pods it owns. - Always include
resources.requestsandresources.limits. Without them the scheduler cannot make good placement decisions and a runaway pod can starve other workloads. - Use an explicit image tag (
1.2.0) rather thanlatest. See the common mistakes section for why this matters.
The ECR image URI format is: <account-id>.dkr.ecr.<region>.amazonaws.com/<repo-name>:<tag>. See Amazon ECR for how to build and push images.
Step 4: Create the Service manifest
A Deployment alone does not make your app reachable. Pod IP addresses change every time a pod restarts or is rescheduled. A Service gives you a stable virtual IP and DNS name that always routes to whichever pods are currently healthy.
# service.yaml
apiVersion: v1
kind: Service
metadata:
name: demo-app
namespace: production
spec:
selector:
app: demo-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIPClusterIP is reachable only inside the cluster. This is the right default for an internal API or backend. To expose the app externally, add a LoadBalancer type Service or an Ingress controller. The Ingress controllers page covers that next step.
The selector: app: demo-app connects the Service to the pods. It must match the pod labels in the Deployment template exactly. A mismatch here is one of the most common reasons apps are unreachable after deployment.
Step 5: Apply the manifests
Apply both files:
kubectl apply -f deployment.yaml
kubectl apply -f service.yamlOr apply an entire directory at once:
kubectl apply -f ./k8s/kubectl apply is idempotent. Running it a second time with no changes makes no changes. Running it after updating the image tag triggers a rolling update. This is why apply is the standard approach: it works the same way whether you are creating a resource for the first time or updating an existing one.
Step 6: Verify the rollout
After applying, confirm the Deployment and pods came up correctly:
# Check the Deployment
kubectl get deployments -n production
# NAME READY UP-TO-DATE AVAILABLE AGE
# demo-app 3/3 3 3 45s
# Check the pods
kubectl get pods -n production
# NAME READY STATUS RESTARTS AGE
# demo-app-7d9f8b-x4klm 1/1 Running 0 45s
# demo-app-7d9f8b-p9wrt 1/1 Running 0 45s
# demo-app-7d9f8b-m2zkq 1/1 Running 0 44s
# Wait for the rollout to complete (blocks until done or failed)
kubectl rollout status deployment/demo-app -n production
# deployment "demo-app" successfully rolled outIf a pod is not healthy, kubectl describe gives you the detail you need:
kubectl describe pod demo-app-7d9f8b-x4klm -n productionLook at the Events section at the bottom of the output. Image pull failures, scheduling failures, and OOMKilled events all appear there first.
Step 7: View logs and debug problems
Once pods are running, use kubectl logs to read their output:
# View current logs for a pod
kubectl logs demo-app-7d9f8b-x4klm -n production
# Stream logs in real time
kubectl logs -f demo-app-7d9f8b-x4klm -n production
# View logs across all pods with a label
kubectl logs -l app=demo-app -n production
# View logs from a previous container instance after a crash
kubectl logs demo-app-7d9f8b-x4klm -n production --previous
# View logs from a specific container in a multi-container pod
kubectl logs demo-app-7d9f8b-x4klm -n production -c sidecarWhen a pod crashes and Kubernetes restarts it, the new pod’s logs do not contain anything from before the crash. The —previous flag shows the logs from the terminated container: the output that actually explains why it crashed. This is the first command to run when a pod is stuck in CrashLoopBackOff.
When a container will not start, check in this order:
kubectl describe pod: look at Events for image pull errors or scheduling failureskubectl logs --previous: check for application startup errors- Check resource limits: is the pod being OOMKilled?
- Check the image URI and tag: does the image actually exist in ECR?
For CrashLoopBackOff specifically, the EKS CrashLoopBackOff troubleshooting guide covers the most common causes.
For structured, persistent log storage, see Logging in Kubernetes on EKS.
Step 8: Port-forward for local testing
Port forwarding lets you reach a pod or Service from your local machine without any external exposure.
# Forward to a pod directly (stops working if that pod is replaced)
kubectl port-forward pod/demo-app-7d9f8b-x4klm 8080:8080 -n production
# Forward to the Service (recommended: works with any healthy pod)
kubectl port-forward service/demo-app 8080:80 -n productionWith either command running, open http://localhost:8080 in a browser or run curl http://localhost:8080.
Port forwarding is like running a temporary extension cable from your laptop directly into the cluster. It is useful for testing, but you would not use an extension cable to power a building permanently. The same applies here: port-forwarding is a debugging tool, not a production access method.
Prefer Service port-forwarding over pod port-forwarding. Pod names include a random suffix that changes when the pod is replaced. Service port-forwarding forwards to any currently healthy pod and keeps working across restarts.
Step 9: Update the image and roll back safely
Updating the image (declarative, preferred)
Update the image tag in deployment.yaml then apply:
# Edit deployment.yaml: change image tag from 1.2.0 to 1.3.0
kubectl apply -f deployment.yamlThe Deployment performs a rolling update, starting new pods before stopping old ones so the app stays online throughout.
Updating the image (imperative, good for quick one-offs)
kubectl set image deployment/demo-app \
demo-app=123456789012.dkr.ecr.eu-west-1.amazonaws.com/demo-app:1.3.0 \
-n productionUse this for quick experiments. For anything repeatable or tracked, update the YAML and apply it.
Monitor the rollout
kubectl rollout status deployment/demo-app -n production
# deployment "demo-app" successfully rolled outView rollout history
kubectl rollout history deployment/demo-app -n production
# REVISION CHANGE-CAUSE
# 1 <none>
# 2 <none>Roll back to the previous version
kubectl rollout undo deployment/demo-app -n productionRoll back to a specific revision
kubectl rollout undo deployment/demo-app --to-revision=1 -n productionRollback works because Kubernetes keeps the old ReplicaSet after a rolling update. kubectl rollout undo reactivates it instantly without rebuilding the image.
When to use this approach
Use raw kubectl when:
- Learning Kubernetes and want to see exactly what each object does
- Debugging a live cluster problem
- Testing a single manifest change quickly
- Validating a small workload before productionising it
Do not rely on raw imperative kubectl as your long-term deployment method when:
- You have multiple environments (dev, staging, production). See Dev vs Staging vs Production for environment management patterns.
- Deployments need approvals or audit trails
- You need repeatability across multiple engineers
- A CI/CD system like GitHub Actions should be the source of truth
Raw kubectl apply against a live cluster is fine for learning and small teams. At scale, it becomes hard to track who deployed what and when. That is where GitOps and CI/CD pipelines take over.
kubectl vs Helm on EKS
Both kubectl and Helm deploy to Kubernetes. They serve different purposes and most teams use both.
kubectl is better for:
- Understanding the raw Kubernetes objects (Deployments, Services, ConfigMaps)
- Direct cluster inspection and debugging
- Simple apps where templating adds no value
- One-off changes and quick rollback commands
Helm is better for:
- Packaging an app with configurable values across environments
- Installing third-party tools (Prometheus, cert-manager, the AWS Load Balancer Controller)
- Distributing apps as versioned, shareable charts
- Teams that want a single
helm rollbackto undo all related resources together
The practical split most teams land on: use kubectl to understand what Kubernetes objects exist and debug live problems; use Helm to install and upgrade applications where you need templated values or chart versioning. You will still use kubectl get, kubectl describe, and kubectl logs even in a fully Helm-managed cluster.
Common mistakes
Wrong kubectl context. Running kubectl against production when you meant staging is easy to do. Always check
kubectl config current-contextbefore creating, updating, or deleting resources.
Fix: Runkubectl config current-contextfirst. Usekubectl config use-contextto switch.Wrong namespace. If a pod, Service, or Deployment is not visible in
kubectl get pods, it is likely in a different namespace. The default namespace is rarely where production workloads live.
Fix: Always specify-n productionor search with—all-namespaces.Using the
latesttag. If you push a new image with thelatesttag but do not change the Deployment spec, Kubernetes sees no change and performs no rollout. The old image keeps running.
Fix: Always use explicit version tags (e.g.,1.3.0or a Git commit SHA). Update the tag in the manifest to trigger a real rollout.Checking only running pods. Crashed pods get replaced. The new pod’s logs do not contain the crash. You can inspect a healthy-looking pod all day and miss the root cause.
Fix: Usekubectl logs —previousto read the logs from the crashed container, andkubectl describe podto see the event history.Deploying without a Service. A running Deployment does not mean the app is reachable. Without a Service there is no stable address and no load balancing.
Fix: Always create a Service alongside a Deployment. Make sure the Serviceselectormatches the podlabelsexactly.Not waiting for rollout status. Applying a manifest returns immediately. “Applied” does not mean “running.” Pods may still be pulling images or failing readiness probes.
Fix: Runkubectl rollout status deployment/demo-app -n productionafter every apply to wait for the rollout to fully complete.
Summary
- Use
aws eks update-kubeconfigto connect kubectl to your EKS cluster before anything else. - Apply both a Deployment and a Service. The Deployment runs pods; the Service makes them reachable.
kubectl apply -f manifest.yamlis idempotent and version-control friendly. Use it for every change.kubectl rollout statustells you when a deployment is actually complete, not just submitted.kubectl logs —previousandkubectl describe podare your primary crash investigation tools.- EKS nodes pull from same-account ECR automatically. No image pull secrets needed for this common case.
kubectl rollout undorolls back a Deployment in seconds without rebuilding the image.
Frequently asked questions
What is the difference between kubectl apply and kubectl create?
kubectl create fails if the resource already exists. kubectl apply creates the resource if it does not exist, or updates it if it does. For anything you will run more than once, use apply: it is idempotent and works well with version-controlled manifests.
How do I deploy a new image version?
Update the image tag in your Deployment YAML and run kubectl apply -f deployment.yaml. Alternatively, use kubectl set image deployment/demo-app demo-app=123456789012.dkr.ecr.eu-west-1.amazonaws.com/demo-app:1.3.0 for a quick one-off update. Always use explicit version tags rather than latest: Kubernetes will not re-pull an image if the tag has not changed.
Do I need imagePullSecrets for ECR?
Not for same-account ECR. EKS managed node groups include the AmazonEC2ContainerRegistryReadOnly policy on the node IAM role by default, so pods pull images from ECR in the same account without extra configuration. You only need imagePullSecrets or IRSA for cross-account ECR access.
Why is my pod running but my app is not reachable?
A running pod does not mean traffic can reach it. The most common causes are: no Service created, the Service selector does not match the pod labels, or you are trying to reach the pod IP directly instead of the Service. Run kubectl get endpoints <service-name> to check whether the Service has discovered the pod.
When should I use Helm instead of raw kubectl?
Use raw kubectl when learning, debugging, or validating a single manifest change. Use Helm when you need to package an application with configurable values, share it across environments, or install third-party tools. Many teams use both: Helm for application packaging, kubectl for direct cluster inspection and debugging.