Home DevOps Ansible for Kubernetes: Automate Cluster Deployments Without the Pain
Advanced 3 min · July 11, 2026

Ansible for Kubernetes: Automate Cluster Deployments Without the Pain

Ansible for Kubernetes: automate cluster provisioning, app deployments, and config management.

N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Production
production tested
July 11, 2026
last updated
1,727
articles · all by Naren
Before you start⏱ 30 min
  • A Kubernetes cluster (local or cloud). kubectl configured and working. Ansible 2.10+ installed.
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

Use Ansible's k8s module to declaratively manage Kubernetes resources. Write playbooks that apply YAML manifests, handle idempotency, and integrate with dynamic inventories. Avoid shelling out to kubectl — use the native module for reliability.

✦ Definition~90s read
What is Ansible for Kubernetes?

Ansible is an agentless automation tool that uses SSH or REST APIs to manage systems. For Kubernetes, it orchestrates cluster setup, application deployments, and configuration management via the k8s module, eliminating manual kubectl commands.

Imagine you're a restaurant manager who needs to set up 50 tables exactly the same way every morning.
Plain-English First

Imagine you're a restaurant manager who needs to set up 50 tables exactly the same way every morning. Instead of walking to each table and placing napkins, silverware, and menus by hand, you write a checklist once and hand it to a team of assistants who each set up a table identically. Ansible is that checklist system — it ensures every Kubernetes cluster gets the same config, every time, without you touching each server.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

You've been burned by shell scripts that work on Monday and fail on Tuesday because someone's kubectl version is different. Or you've watched a 200-line bash script silently skip a step when a pod name changed. That's where Ansible for Kubernetes comes in — it's not just automation, it's idempotent, declarative, and testable. This article gives you battle-tested patterns to provision clusters, deploy apps, and manage configs without the fragility of ad-hoc scripts. By the end, you'll write playbooks that survive a 3am pager and a junior engineer's first commit.

Why Ansible for Kubernetes Beats Shell Scripts

Shell scripts with kubectl are the duct tape of DevOps. They work until they don't — a typo in a grep, a missing pipe, a race condition when two scripts run simultaneously. Ansible gives you idempotency out of the box: run the same playbook ten times, get the same result. The k8s module communicates directly with the Kubernetes API, so you skip the kubectl binary dependency and get proper error handling. Plus, you can integrate with dynamic inventories from cloud providers, manage secrets via Ansible Vault, and reuse roles across environments. The problem it solves is simple: repeatable, auditable, and debuggable cluster management.

deploy-webapp.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# io.thecodeforge — DevOps tutorial
---
- name: Deploy web application to Kubernetes
  hosts: localhost
  gather_facts: no
  vars:
    namespace: production
    app_name: webapp
    image: registry.example.com/webapp:v1.2.3
    replicas: 3
  tasks:
    - name: Ensure namespace exists
      kubernetes.core.k8s:
        name: "{{ namespace }}"
        api_version: v1
        kind: Namespace
        state: present

    - name: Deploy application
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: "{{ app_name }}"
            namespace: "{{ namespace }}"
          spec:
            replicas: "{{ replicas }}"
            selector:
              matchLabels:
                app: "{{ app_name }}"
            template:
              metadata:
                labels:
                  app: "{{ app_name }}"
              spec:
                containers:
                  - name: "{{ app_name }}"
                    image: "{{ image }}"
                    ports:
                      - containerPort: 8080
                    resources:
                      requests:
                        memory: "256Mi"
                        cpu: "250m"
                      limits:
                        memory: "512Mi"
                        cpu: "500m"
        wait: yes
        wait_timeout: 120
Output
PLAY [Deploy web application to Kubernetes] *******************************
TASK [Ensure namespace exists] **********************************************
ok: [localhost]
TASK [Deploy application] ***************************************************
changed: [localhost]
PLAY RECAP ******************************************************************
localhost : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Senior Shortcut:
Use wait: yes and wait_timeout to block until the deployment rolls out. Without it, the task returns as soon as the API accepts the definition — not when pods are ready.

Dynamic Inventories: Don't Hardcode Cluster Endpoints

Hardcoding cluster IPs in your inventory is a disaster waiting to happen — clusters get recycled, IPs change, and you end up deploying to the wrong environment. Use dynamic inventory scripts or plugins to source cluster info from your cloud provider or a CMDB. For Kubernetes, the simplest approach is to use the kubeconfig file on the Ansible controller, but that ties you to a single cluster. Instead, use the kubernetes.core.k8s_cluster_info module to discover clusters, or pass the kubeconfig path as a variable per environment. The why: your playbooks become environment-agnostic and safe to run in CI/CD.

dynamic-inventory.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# io.thecodeforge — DevOps tutorial
# Example dynamic inventory script (Python) that returns cluster endpoints
# Save as inventory/k8s_clusters.py and make executable
#!/usr/bin/env python3
import json

# In real life, query AWS EKS, GKE, or your CMDB
def get_clusters():
    return {
        "production": {
            "hosts": ["k8s-prod-1.example.com"],
            "vars": {
                "kubeconfig": "/etc/ansible/kubeconfigs/prod.config",
                "namespace": "production"
            }
        },
        "staging": {
            "hosts": ["k8s-staging-1.example.com"],
            "vars": {
                "kubeconfig": "/etc/ansible/kubeconfigs/staging.config",
                "namespace": "staging"
            }
        }
    }

if __name__ == "__main__":
    print(json.dumps(get_clusters()))
Output
{
"production": {
"hosts": ["k8s-prod-1.example.com"],
"vars": {
"kubeconfig": "/etc/ansible/kubeconfigs/prod.config",
"namespace": "production"
}
},
"staging": {
"hosts": ["k8s-staging-1.example.com"],
"vars": {
"kubeconfig": "/etc/ansible/kubeconfigs/staging.config",
"namespace": "staging"
}
}
}
Production Trap:
Never store kubeconfig files with cluster-admin credentials in version control. Use Ansible Vault or a secrets manager like HashiCorp Vault to inject them at runtime.

Idempotency and State Management: The k8s Module Under the Hood

The k8s module is idempotent by design — it compares the desired state (your definition) with the current state on the cluster and applies only the diff. Under the hood, it uses the Kubernetes Python client to perform a strategic merge patch or a JSON patch, depending on the resource type. This means you can run the same playbook repeatedly without side effects. However, there's a gotcha: the module doesn't track fields it doesn't know about. If you omit a field that the cluster already has, the module won't remove it — it only adds or updates fields you specify. To enforce exact state, set state: patched with definition that includes all fields you want, or use state: present with apply: yes which does a full replace.

idempotent-deploy.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# io.thecodeforge — DevOps tutorial
---
- name: Idempotent deployment with exact state
  hosts: localhost
  vars:
    namespace: production
    app_name: webapp
  tasks:
    - name: Apply deployment with full spec (replaces if changed)
      kubernetes.core.k8s:
        state: present
        apply: yes  # Forces full replace, not patch
        definition:
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: "{{ app_name }}"
            namespace: "{{ namespace }}"
          spec:
            replicas: 3
            selector:
              matchLabels:
                app: "{{ app_name }}"
            template:
              metadata:
                labels:
                  app: "{{ app_name }}"
              spec:
                containers:
                  - name: "{{ app_name }}"
                    image: registry.example.com/webapp:v1.2.3
                    ports:
                      - containerPort: 8080
        wait: yes
        wait_timeout: 120
Output
TASK [Apply deployment with full spec] **************************************
changed: [localhost] # Only changes if the spec differs from cluster state
Interview Gold:
Q: How does Ansible's k8s module handle idempotency? A: It uses the Kubernetes Python client to fetch current state, computes a strategic merge patch, and applies only the diff. For full replace, use apply: yes.

Managing Secrets: Ansible Vault vs. External Secrets Operators

Never put plaintext secrets in your playbooks or inventory. Ansible Vault encrypts variables and files at rest, but decryption happens on the controller — meaning the secret is in memory during execution. For Kubernetes, the better pattern is to use an external secrets operator (e.g., External Secrets Operator, Sealed Secrets) and let Ansible only manage the reference. This way, the actual secret never touches your playbook. If you must use Vault, encrypt the entire variable file and use include_vars with the vault password file. The gotcha: vault-encrypted files can't be diffed in code review — use a secrets manager instead.

secrets-management.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# io.thecodeforge — DevOps tutorial
---
- name: Deploy with external secrets
  hosts: localhost
  vars:
    namespace: production
    app_name: webapp
    # Reference to external secret, not the value itself
    db_password_ref:
      name: db-credentials
      key: password
  tasks:
    - name: Create ExternalSecret resource
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: external-secrets.io/v1beta1
          kind: ExternalSecret
          metadata:
            name: "{{ app_name }}-db"
            namespace: "{{ namespace }}"
          spec:
            refreshInterval: 1h
            secretStoreRef:
              name: vault-backend
              kind: SecretStore
            target:
              name: "{{ app_name }}-db"
            data:
              - secretKey: password
                remoteRef:
                  key: secrets/{{ app_name }}/db
                  property: password

    - name: Deploy app that mounts the secret
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: "{{ app_name }}"
            namespace: "{{ namespace }}"
          spec:
            template:
              spec:
                containers:
                  - name: "{{ app_name }}"
                    env:
                      - name: DB_PASSWORD
                        valueFrom:
                          secretKeyRef:
                            name: "{{ app_name }}-db"
                            key: password
Output
TASK [Create ExternalSecret resource] ***************************************
changed: [localhost]
TASK [Deploy app that mounts the secret] *************************************
changed: [localhost]
Never Do This:
Don't use k8s module with definition containing stringData with plaintext secrets. Anyone with access to the playbook can read them. Always use external secret stores.

Rolling Updates and Canary Deployments with Ansible

Ansible can orchestrate complex rollout strategies by combining the k8s module with loops and conditionals. For a canary deployment, you create two deployments (stable and canary) and a service that routes a percentage of traffic to the canary. Use Ansible to gradually increase the canary replicas and monitor error rates before promoting. The key is to use wait with wait_condition to check pod readiness. Without Ansible, you'd manually run kubectl commands and watch — with Ansible, it's a single playbook run.

canary-deploy.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# io.thecodeforge — DevOps tutorial
---
- name: Canary deployment with gradual traffic shift
  hosts: localhost
  vars:
    namespace: production
    app_name: webapp
    canary_percentage: 10  # Start with 10%
  tasks:
    - name: Deploy canary with limited replicas
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: "{{ app_name }}-canary"
            namespace: "{{ namespace }}"
            labels:
              app: "{{ app_name }}"
              track: canary
          spec:
            replicas: "{{ (canary_percentage / 100 * 10) | int }}"  # 10% of 10 = 1 replica
            selector:
              matchLabels:
                app: "{{ app_name }}"
                track: canary
            template:
              metadata:
                labels:
                  app: "{{ app_name }}"
                  track: canary
              spec:
                containers:
                  - name: "{{ app_name }}"
                    image: registry.example.com/webapp:v2.0.0-rc1
        wait: yes
        wait_timeout: 120

    - name: Update service to include canary selector
      kubernetes.core.k8s:
        state: present
        definition:
          apiVersion: v1
          kind: Service
          metadata:
            name: "{{ app_name }}"
            namespace: "{{ namespace }}"
          spec:
            selector:
              app: "{{ app_name }}"
              # No track label — routes to both stable and canary
        wait: yes
Output
TASK [Deploy canary with limited replicas] **********************************
changed: [localhost]
TASK [Update service to include canary selector] *****************************
changed: [localhost]
Senior Shortcut:
Use wait_condition to check for specific pod statuses. For canary, wait for condition=Ready, status=True before increasing traffic.

Error Handling and Rollback Strategies

When a deployment fails, you need to roll back fast. Ansible's block and rescue let you catch failures and execute rollback tasks. For example, if the canary deployment doesn't become ready, rescue can delete the canary and restore the original service selector. The gotcha: the k8s module doesn't automatically roll back on failure — you must implement it yourself. Also, use ignore_errors: yes sparingly; it's better to use failed_when with specific conditions.

rollback.ymlYAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# io.thecodeforge — DevOps tutorial
---
- name: Deploy with automatic rollback
  hosts: localhost
  vars:
    namespace: production
    app_name: webapp
  tasks:
    - block:
        - name: Deploy new version
          kubernetes.core.k8s:
            state: present
            definition:
              apiVersion: apps/v1
              kind: Deployment
              metadata:
                name: "{{ app_name }}"
                namespace: "{{ namespace }}"
              spec:
                replicas: 3
                selector:
                  matchLabels:
                    app: "{{ app_name }}"
                template:
                  metadata:
                    labels:
                      app: "{{ app_name }}"
                  spec:
                    containers:
                      - name: "{{ app_name }}"
                        image: registry.example.com/webapp:v2.0.0
            wait: yes
            wait_timeout: 120
      rescue:
        - name: Rollback to previous version
          kubernetes.core.k8s:
            state: present
            definition:
              apiVersion: apps/v1
              kind: Deployment
              metadata:
                name: "{{ app_name }}"
                namespace: "{{ namespace }}"
              spec:
                template:
                  spec:
                    containers:
                      - name: "{{ app_name }}"
                        image: registry.example.com/webapp:v1.2.3  # Previous stable version
            wait: yes
            wait_timeout: 120
        - name: Notify team
          debug:
            msg: "Rollback triggered for {{ app_name }} in {{ namespace }}"
Output
TASK [Deploy new version] ***************************************************
fatal: [localhost]: FAILED! => ...
TASK [Rollback to previous version] ******************************************
changed: [localhost]
TASK [Notify team] ***********************************************************
ok: [localhost] => {
"msg": "Rollback triggered for webapp in production"
}
Production Trap:
Don't rely on Kubernetes' default rollback (kubectl rollout undo). It only works if the previous revision exists. Ansible's rescue block gives you explicit control over which version to roll back to.

When Not to Use Ansible for Kubernetes

Ansible is overkill for simple, one-off deployments — use kubectl directly. It's also not ideal for real-time cluster autoscaling or event-driven operations; use a Kubernetes operator or a controller instead. If your team is already invested in Helm, Ansible can still manage Helm releases via the helm module, but writing raw YAML in Ansible may duplicate effort. Finally, avoid Ansible for day-2 operations like pod eviction or node draining — those are better handled by Kubernetes-native tools or scripts.

Interview Gold:
Q: When would you choose Ansible over Helm for Kubernetes deployments? A: Ansible is better when you need to orchestrate multi-step workflows (e.g., pre-deploy DB migrations, post-deploy smoke tests) that Helm charts alone can't handle.
● Production incidentPOST-MORTEMseverity: high

The 4GB Container That Kept Dying

Symptom
A stateless Go service crashed every 6 hours with OOMKilled. Memory limit was 512Mi, but the container's RSS grew to 4GB before the kill.
Assumption
The team assumed a memory leak in the app — they spent two weeks profiling heap dumps.
Root cause
The Ansible playbook that deployed the service used k8s module with definition from a Jinja2 template. The template had a typo: resources.limits.memory was set to 512Mi but resources.requests.memory was missing, so Kubernetes set requests equal to limits. However, the Go app had a goroutine that spawned a subprocess reading large files — that subprocess wasn't accounted for in cgroup limits, so it consumed host memory until OOM.
Fix
Add resources.requests.memory: 256Mi explicitly and set memory_swap: 0 in the container spec. Also added securityContext.runAsNonRoot: true to prevent subprocess privilege escalation.
Key lesson
  • Always set both requests and limits explicitly.
  • Never assume a missing request defaults to zero — it defaults to the limit.
Production debug guideSystematic recovery paths for the failure modes engineers actually hit.3 entries
Symptom · 01
Task fails with 'HTTP 409 Conflict' when creating a resource
Fix
1. Check if resource already exists. 2. Use state: present instead of state: absent then present. 3. Ensure apiVersion and kind match exactly.
Symptom · 02
Ansible hangs on 'Waiting for deployment to become ready'
Fix
1. Check pod logs: kubectl logs <pod>. 2. Verify resource requests/limits. 3. Increase wait_timeout. 4. Check node capacity.
Symptom · 03
k8s module returns 'Failed to parse definition'
Fix
1. Validate YAML syntax. 2. Check indentation. 3. Use definition as a dict, not a string. 4. Run kubectl apply --dry-run=client to test.
★ Ansible for Kubernetes Triage Cheat SheetFirst-response commands for when things go wrong — copy-paste ready.
Task fails with `Failed to import kubernetes`
Immediate action
Check if the Python client is installed on the Ansible controller
Commands
pip show kubernetes
pip install kubernetes
Fix now
Install the kubernetes Python package: pip install kubernetes
`kubeconfig` not found or invalid+
Immediate action
Verify the kubeconfig path and cluster access
Commands
kubectl --kubeconfig=/path/to/config cluster-info
ansible localhost -m kubernetes.core.k8s_cluster_info -a 'kubeconfig=/path/to/config'
Fix now
Correct the kubeconfig path in inventory or playbook vars
Deployment never becomes ready+
Immediate action
Check pod events and logs
Commands
kubectl describe pod -l app=webapp
kubectl logs -l app=webapp --tail=50
Fix now
Fix the container image or resource limits, then re-run playbook
`state: absent` doesn't delete the resource+
Immediate action
Verify the resource name and namespace
Commands
kubectl get deployment -n production webapp
ansible localhost -m kubernetes.core.k8s -a 'name=webapp namespace=production api_version=apps/v1 kind=Deployment state=absent'
Fix now
Ensure the name and namespace match exactly, including case
Feature / AspectAnsible k8s Modulekubectl apply
IdempotencyBuilt-in (strategic merge patch)Manual (kubectl apply is idempotent but no diff logic)
Error handlingAnsible block/rescue, retriesShell exit codes, manual scripting
Secret managementAnsible Vault or external operatorskubectl create secret, no built-in encryption
Dynamic inventoryYes (plugins, scripts)No (static kubeconfig)
RollbackCustom rescue taskskubectl rollout undo
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
deploy-webapp.yml- name: Deploy web application to KubernetesWhy Ansible for Kubernetes Beats Shell Scripts
dynamic-inventory.ymldef get_clusters():Dynamic Inventories
idempotent-deploy.yml- name: Idempotent deployment with exact stateIdempotency and State Management
secrets-management.yml- name: Deploy with external secretsManaging Secrets
canary-deploy.yml- name: Canary deployment with gradual traffic shiftRolling Updates and Canary Deployments with Ansible
rollback.yml- name: Deploy with automatic rollbackError Handling and Rollback Strategies

Key takeaways

1
Use kubernetes.core collection with pip install kubernetes.
2
The k8s module is idempotent and supports all Kubernetes resource types.
3
Ansible can complement Helm and GitOps workflows for Kubernetes.
4
Use Ansible Vault to encrypt Kubernetes secrets in playbooks.

Common mistakes to avoid

3 patterns
×

Not installing Python Kubernetes client

Fix
The kubernetes.core collection requires pip install kubernetes. Without it, all k8s module calls fail with import errors.
×

Using shell/command for kubectl calls

Fix
Use the dedicated k8s module instead of kubectl via shell. The k8s module is idempotent and returns structured results.
×

Storing Kubernetes secrets unencrypted in playbooks

Fix
Use Ansible Vault or HashiCorp Vault lookup to manage Kubernetes secrets. Never store base64-encoded secrets in Git.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does Ansible's k8s module handle idempotency under the hood?
Q02SENIOR
When would you choose Ansible over Helm for Kubernetes deployments in a ...
Q03SENIOR
What happens when you run the same Ansible playbook twice with the k8s m...
Q04JUNIOR
What is the difference between `state: present` and `state: patched` in ...
Q05SENIOR
You have an Ansible playbook that deploys a ConfigMap. The ConfigMap is ...
Q06SENIOR
How would you design an Ansible-based system to manage Kubernetes cluste...
Q01 of 06SENIOR

How does Ansible's k8s module handle idempotency under the hood?

ANSWER
It uses the Kubernetes Python client to fetch the current state of the resource, computes a strategic merge patch between the desired and current state, and applies only the diff. This avoids unnecessary API calls and ensures no side effects on re-runs.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can Ansible manage Kubernetes resources?
02
How do I install the Kubernetes collection?
03
Can Ansible install a Kubernetes cluster?
04
How does Ansible compare to Helm for Kubernetes management?
05
Can Ansible implement GitOps workflows for Kubernetes?
06
How do I manage Kubernetes secrets with Ansible?
N
Naren Founder & Principal Engineer

20+ years shipping production infrastructure and CI/CD at scale. Drawn from code that ran under real load.

Follow
Verified
production tested
July 11, 2026
last updated
1,727
articles · all by Naren
🔥

That's Ansible. Mark it forged?

3 min read · try the examples if you haven't

Previous
Configuration Drift and Compliance
37 / 37 · Ansible