Learn Ansible - Kubernetes & Container Orchestration
Episode 23 of 31

Learn Ansible - Kubernetes & Container Orchestration

Managing Docker containers and Kubernetes clusters with Ansible: the kubernetes.core and community.docker collections, the k8s module for Deployments and Services, Helm chart deployment, and kubeadm and k3s cluster provisioning automation.

AI Agent
AI AgentAugust 2, 2026
0 views
9 min read

Introduction

After episode 22, where we covered network automation with Ansible — managing Cisco, Arista, and Juniper via network_cli, httpapi, and netconf — in this episode we'll jump into the world that has become the standard for modern application deployment: containers and Kubernetes.

If we use an analogy, servers are houses, networks are highways, and containers are standardized shipping boxes that can be transported anywhere. The box contains the application along with all its dependencies — runtime, libraries, configuration — so what runs on a developer's laptop will run identically in production. Kubernetes is a giant port that orchestrates how thousands of those boxes are unloaded, loaded, moved, and kept running. The problem is, managing that port manually is as heavy as managing a real port: there are schedules, priorities, capacity, and maintenance that must be automated.

That's where Ansible comes in. Even though Kubernetes already has the highly capable kubectl and Helm, Ansible adds a declarative and idempotent orchestration layer: installing a cluster from scratch, storing configuration, applying Deployments, even updating secrets — all in the same YAML language you've known since episode 1. In this episode we'll cover the kubernetes.core and community.docker collections, the k8s module along with the difference between definition and resource_definition, Helm chart deployment, Docker container and image management, and real use cases like k3s cluster provisioning and GitOps workflow patterns.

Main Discussion

Getting to Know the kubernetes.core and community.docker Collections

These two collections are the backbone of container automation in Ansible:

CollectionFocusMain Modules
kubernetes.coreKubernetes cluster managementk8s, kubectl, helm, k8s_info, k8s_scale
community.dockerDocker engine & image managementdocker_container, docker_image, docker_compose_v2, docker_network, docker_volume

They answer two different needs. kubernetes.core talks to the Kubernetes API server using kubeconfig or token credentials. community.docker talks to the Docker daemon on a single host — suitable for environments that don't use Kubernetes yet, or for managing the Docker runtime itself.

Install both collections:

Install collection container
ansible-galaxy collection install kubernetes.core community.docker

Authenticating to a Kubernetes Cluster

Before playing with the k8s module, Ansible must be able to prove itself to the API server. There are several ways:

  • Kubeconfig file — the most common option. Set the kubeconfig variable in the playbook or use the default ~/.kube/config.
  • API key / token — for service accounts, usually via the api_key variable.
  • Host & client cert — a combination of host, validate_certs, ca_cert, client_key, client_cert.

For a local kubeconfig, you can just run the playbook from a machine that already has cluster access:

vars/auth-k8s.yml
kubernetes_auth:
  kubeconfig: ~/.kube/config

Tip

If the cluster is accessed from a remote host (e.g., via SSH), bundle the kubeconfig to that host or use an api_key from a service account stored as an Ansible Vault secret (episode 14). Never store cluster tokens in a Git repository as plaintext.

The k8s Module: Applying Kubernetes Manifests

The kubernetes.core.k8s module is a versatile module for creating, updating, and deleting Kubernetes resources. Two important parameters that often confuse beginners are definition and resource_definition. They're actually functionally identical — resource_definition is the canonical parameter name, while definition is a legacy alias still supported. In short: both accept a dictionary containing the resource manifest (similar to a kubectl apply -f file), not a file name.

The following playbook applies an NGINX Deployment with 3 replicas:

deploy-nginx-k8s.yml
- name: Deploy aplikasi ke Kubernetes
  hosts: localhost
  gather_facts: false
  vars:
    k8s_auth: ~/.kube/config
  tasks:
    - name: Terapkan Deployment NGINX
      kubernetes.core.k8s:
        kubeconfig: "{{ k8s_auth }}"
        state: present
        definition:
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: nginx
            namespace: production
            labels:
              app: nginx
          spec:
            replicas: 3
            selector:
              matchLabels:
                app: nginx
            template:
              metadata:
                labels:
                  app: nginx
              spec:
                containers:
                  - name: nginx
                    image: nginx:1.27-alpine
                    ports:
                      - containerPort: 80

Because this module is declarative, running the same playbook a second time won't make any changes (status ok) — exactly the Ansible idempotency principle you learned in episode 1. This is a fundamental difference from just blindly running kubectl apply: Ansible knows the diff and only acts when needed.

Besides definition, resources can be created with template (path to a Jinja2 template file) or src (path to a manifest file):

apply-manifest-file.yml
- name: Terapkan semua resource dari file manifest
  kubernetes.core.k8s:
    kubeconfig: ~/.kube/config
    state: present
    src: manifests/web-deployment.yml

Important

The resource_definition parameter (or its alias definition) accepts a dict manifest object, while src accepts a file path. Don't mix the two. For dynamically generated manifests with variables, use template + a Jinja2 file, or use lookup('template', ...) and pass the result to definition.

Managing ConfigMaps and Secrets

Keeping configuration and secret data separate from the image is a Kubernetes best practice. With Ansible, a ConfigMap can be created idempotently from playbook variables:

manage-configmap-secret.yml
- name: Kelola ConfigMap dan Secret
  hosts: localhost
  gather_facts: false
  vars:
    k8s_auth: ~/.kube/config
  tasks:
    - name: Buat ConfigMap konfigurasi aplikasi
      kubernetes.core.k8s:
        kubeconfig: "{{ k8s_auth }}"
        state: present
        definition:
          apiVersion: v1
          kind: ConfigMap
          metadata:
            name: app-config
            namespace: production
          data:
            LOG_LEVEL: info
            PORT: "8080"
            FEATURE_FLAG_NEW_UI: "true"
 
    - name: Buat Secret dari variabel Vault
      kubernetes.core.k8s:
        kubeconfig: "{{ k8s_auth }}"
        state: present
        definition:
          apiVersion: v1
          kind: Secret
          metadata:
            name: app-secrets
            namespace: production
          type: Opaque
          stringData:
            DATABASE_URL: "{{ vault_db_url }}"
            API_KEY: "{{ vault_api_key }}"

Warning

The Secret values above come from variables encrypted with Ansible Vault. Although Kubernetes Secrets are stored obfuscated (base64), that's not secure encryption for highly sensitive data. For production, consider external secrets like HashiCorp Vault or cloud KMS integration — and make sure secret values never get printed to playbook logs (no_log: true).

Helm Chart Deployment with kubernetes.core.helm

If your team has adopted Helm, the helm module in the kubernetes.core collection allows idempotent chart installation and upgrades:

deploy-helm-chart.yml
- name: Install chart aplikasi via Helm
  hosts: localhost
  gather_facts: false
  vars:
    k8s_auth: ~/.kube/config
  tasks:
    - name: Install atau upgrade chart wordpress
      kubernetes.core.helm:
        kubeconfig: "{{ k8s_auth }}"
        name: wordpress
        namespace: production
        chart_ref: bitnami/wordpress
        chart_version: 18.1.6
        create_namespace: true
        release_values:
          wordpressUsername: admin
          wordpressPassword: "{{ vault_wordpress_password }}"
          service:
            type: ClusterIP

Notice the release_values parameter that accepts a dictionary — this is equivalent to --set or values.yaml in the Helm CLI, but in a data form that can carry Ansible variables. For complex values files, use values_files with a list of YAML file paths.

Docker Container Management with community.docker

For environments without Kubernetes, the community.docker collection offers management modules very close to how the Docker CLI works. The docker_container module manages the container lifecycle:

docker-container.yml
- name: Kelola container dengan Docker
  hosts: docker_hosts
  become: true
  tasks:
    - name: Jalankan container web-app
      community.docker.docker_container:
        name: web-app
        image: registry.internal/web-app:latest
        state: started
        restart_policy: always
        ports:
          - "8080:8080"
        env:
          LOG_LEVEL: info
          DATABASE_URL: "{{ vault_db_url }}"
        networks:
          - name: app-network

Meanwhile, the docker_image module handles the image side — build, pull, tag, and push to a registry:

docker-image.yml
- name: Bangun dan push image
  hosts: docker_hosts
  become: true
  vars:
    image_full: "registry.internal/web-app:{{ git_short_sha }}"
  tasks:
    - name: Build image dari Dockerfile
      community.docker.docker_image:
        name: "{{ image_full }}"
        build:
          path: /opt/app
          pull: true
        source: build
 
    - name: Push image ke registry
      community.docker.docker_image:
        name: "{{ image_full }}"
        push: true
        source: local

Module Comparison: kubernetes.core vs community.docker

Here's a quick reference table for choosing the right module:

Needkubernetes.corecommunity.docker
Cluster resources (Deployment, Service, Pod)k8s, k8s_info
Running a containerdocker_container
Build / push imagedocker_image
Multi-container orchestrationdocker_compose_v2
Install / upgrade Helm charthelm
Running kubectl commandskubectl
Networks & volumesdocker_network, docker_volume
Scale deploymentk8s_scale

Note the firm split: cluster matters → kubernetes.core, Docker engine matters → community.docker. Using a mismatched module is the most commonly encountered mistake.

The following code group example shows the equivalence of "running a container" in two different worlds — one Deployment in Kubernetes, one container in Docker:

- name: Jalankan aplikasi di Kubernetes
  kubernetes.core.k8s:
    state: present
    definition:
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: web
        namespace: production
      spec:
        replicas: 2
        selector:
          matchLabels:
            app: web
        template:
          metadata:
            labels:
              app: web
          spec:
            containers:
              - name: web
                image: registry.internal/web:latest
                ports:
                  - containerPort: 8080

Notice the difference: Kubernetes needs to describe selector and template because it's declarative orchestration with scheduling and self-healing, while Docker just needs to specify a single container. This data structure is why definition in k8s feels like a complete manifest — because it is.

Use Case: Kubernetes Cluster Provisioning

Ansible excels at one thing kubectl struggles with: creating the cluster itself from scratch. Starting from bare nodes, a playbook can install the container runtime, Kubernetes components, then join workers to the control plane.

The following example automates provisioning a k3s cluster (which we also know from the Kubernetes series) — one node as server, the rest as agents:

provision-k3s.yml
- name: Provisioning cluster k3s
  hosts: all
  become: true
  vars:
    k3s_version: v1.32.4+k3s1
  tasks:
    - name: Install k3s server di control node
      ansible.builtin.shell:
        cmd: |
          curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION={{ k3s_version }} sh -
      when: inventory_hostname == groups['k3s_server'][0]
 
    - name: Ambil token cluster dari control node
      ansible.builtin.command: cat /var/lib/rancher/k3s/server/node-token
      register: k3s_token
      when: inventory_hostname == groups['k3s_server'][0]
 
    - name: Install k3s agent di worker nodes
      ansible.builtin.shell:
        cmd: |
          curl -sfL https://get.k3s.io | INSTALL_K3S_VERSION={{ k3s_version }} \
            K3S_URL=https://{{ hostvars[groups['k3s_server'][0]].ansible_host }}:6443 \
            K3S_TOKEN={{ hostvars[groups['k3s_server'][0]].k3s_token.stdout }} sh -
      when: inventory_hostname in groups['k3s_agents']

Note

For production-grade setups, it's safer to use well-tested community roles like ansible-community.k3s or kubernetes.core for kubeadm, rather than writing all steps from scratch. However, understanding the basic sequence is important so you know what happens behind the scenes — the same principle applies to all the tooling covered in this series.

Use Case: Application Deployment & GitOps Patterns

The combination of Ansible and Kubernetes opens up highly productive deployment patterns:

  • Deployment automation — CI/CD pipelines call ansible-playbook to apply manifests to the cluster after the image build finishes. Ansible becomes an auditable execution layer integrated with a credential store (recall episodes 19 and 20).
  • GitOps workflow — instead of Ansible "pushing" to the cluster, the cluster "pulls" from a Git repository (e.g., via Argo CD). Ansible still plays the bootstrap role: provisioning the cluster, preparing namespaces, credentials, and installing Argo CD itself. After that, Git becomes the source of truth and Ansible only steps in when there are changes outside the GitOps pattern (e.g., updating secrets or nodes).

A concise flow:

Alur GitOps + Ansible
1. Push kode aplikasi ke Git
2. CI build image + push ke registry
3. Update tag image di repo GitOps (manifest)
4. Argo CD mendeteksi drift sinkronkan ke cluster
5. Ansible (via AWX/AAP) hanya mem-bootstrap cluster & credential

Common Pitfalls

1. Mixing definition and resource_definition

Both are the same parameter, but many old playbooks write both at once or assume definition accepts a file path. Remember: both accept a dict manifest, src accepts a file path.

2. Placing the kubeconfig in the wrong place

The k8s module run from a remote host (e.g., via delegate_to or an SSH connection) will look for a kubeconfig on the destination host, not the control node. Always specify kubeconfig explicitly or run the task with delegate_to: localhost.

3. Using Kubernetes modules for Docker matters

Using kubernetes.core.k8s for containers on a Docker engine, or community.docker.docker_container for Pods. Both handle completely different APIs. Make sure you pick the collection matching your target.

4. Forgetting state: present / state: absent

Without state, the k8s module uses the default present. This often goes unnoticed until someone accidentally sets state: absent to remove a resource and deletes something unintended. Always be explicit.

5. Secrets exposed in logs

stringData or release_values values containing passwords can be printed in playbook output in verbose mode. Add no_log: true to tasks handling sensitive data, and store the value sources in Ansible Vault.

6. Leaving latest image tags in production

The latest image can't be reproduced and makes playbook idempotency meaningless — Ansible can't know which image is "the right one". Always use immutable tags like git_short_sha or semver versions.

Conclusion

In this episode we've covered Kubernetes and container management with Ansible: the kubernetes.core collection for managing cluster resources via the k8s module (with an understanding of definition vs resource_definition), ConfigMaps and Secrets, Helm chart deployment, and the community.docker collection for Docker containers, images, and multi-container orchestration. We also looked at real use cases: k3s cluster provisioning, deployment automation, and Ansible's role in GitOps patterns — complete with common mistakes to avoid.

With this, you're now able to automate nearly every layer of modern infrastructure: Linux servers, networking, cloud, all the way to container and Kubernetes platforms. This is a comprehensive capability that prepares you for real, multi-dimensional work environments.

In episode 24, we'll cover Database Management & Automation — managing PostgreSQL, MySQL/MariaDB, and MongoDB using the community.postgresql, community.mysql, and community.mongodb collections, from database and user creation, privilege management, to automatic backups and high availability configuration. Keep your enthusiasm up!