Comprehensive Ansible with K8s

Comprehensive Ansible with K8s
Photo by Growtika / Unsplash

This article will guides you through using Ansible, starting from a single, simple playbook and progressing to a professional, role-based structure. We'll deploy common infrastructure including Docker, WordPress, n8n, and finally, a Kubernetes (K8s) cluster with a control plane and a worker agent.

Note: This tutorial uses modern Ansible practices, including Fully Qualified Collection Names (FQCNs) and the latest recommended methods for managing APT repositories.

Prerequisites

  • A control node (your machine) with Ansible installed (sudo apt install ansible or pip install ansible).
  • Target nodes: At least two Ubuntu Linux VMs (e.g., 192.168.1.100 for master/general apps, 192.168.1.101 for the K8s worker agent) with SSH access configured via keys.
  • Basic understanding of YAML syntax.

Install required Ansible collections:

ansible-galaxy collection install community.docker kubernetes.core

Part 1: The Basics - Inventory and Ad-Hoc Commands

Ansible needs to know where to run playbooks. This is defined in an inventory file. We will define a general group, and specific groups for our K8s setup.

1. Create an Inventory

Create a file named inventory.ini:

[webservers]
192.168.1.100

[k8s_master]
192.168.1.100

[k8s_agents]
192.168.1.101

# Grouping all k8s nodes together
[k8s_cluster:children]
k8s_master
k8s_agents

[all:vars]
ansible_user=ubuntu
ansible_python_interpreter=/usr/bin/python3

2. The ansible CLI (Ad-Hoc Commands)

Use the ansible command for quick tasks.

Ping all servers to test connectivity:

ansible all -i inventory.ini -m ping

Check system uptime on the K8s agents:

ansible k8s_agents -i inventory.ini -a "uptime"

Part 2: The Single Playbook (ansible-playbook)

Playbooks (.yml files) are the core of Ansible. Let's write one to install Docker using modern APT repository standards (avoiding the deprecated apt_key module).

1. Write the Playbook

Create a file named setup_docker.yml.

---
- name: Install and Configure Docker
  hosts: webservers
  become: yes
  
  tasks:
    - name: Update apt cache and install prerequisites
      ansible.builtin.apt:
        pkg:
          - ca-certificates
          - curl
          - gnupg
          - python3-pip
        state: present
        update_cache: yes

    - name: Create directory for Docker GPG key
      ansible.builtin.file:
        path: /etc/apt/keyrings
        state: directory
        mode: '0755'

    - name: Download Docker GPG key
      ansible.builtin.get_url:
        url: https://download.docker.com/linux/ubuntu/gpg
        dest: /etc/apt/keyrings/docker.asc
        mode: '0644'

    - name: Add Docker Repository
      ansible.builtin.apt_repository:
        repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu focal stable"
        state: present

    - name: Install Docker CE and plugins
      ansible.builtin.apt:
        pkg:
          - docker-ce
          - docker-ce-cli
          - containerd.io
          - docker-compose-plugin # Modern Docker Compose V2
        state: latest
        update_cache: yes

    - name: Ensure Docker service is running
      ansible.builtin.systemd:
        name: docker
        state: started
        enabled: yes

2. Run the Playbook

ansible-playbook -i inventory.ini setup_docker.yml

Part 3: Structuring with Roles (Apps & Automations)

As your infrastructure grows, Roles break complex playbooks into reusable components.

1. Create the Directory Structure

mkdir my-ansible-project && cd my-ansible-project
mkdir roles && cd roles
ansible-galaxy init docker
ansible-galaxy init wordpress
ansible-galaxy init n8n
cd ..

(Move the tasks from Part 2 into roles/docker/tasks/main.yml)

2. The wordpress Role (Modern Compose V2)

Let's deploy WordPress using Docker Compose V2.

# roles/wordpress/tasks/main.yml
---
- name: Create WordPress directory
  ansible.builtin.file:
    path: /opt/wordpress
    state: directory

- name: Create docker-compose.yml for WordPress
  ansible.builtin.copy:
    dest: /opt/wordpress/docker-compose.yml
    content: |
      services:
        wordpress:
          image: wordpress:latest
          restart: always
          ports:
            - 8080:80
          environment:
            WORDPRESS_DB_HOST: db
            WORDPRESS_DB_USER: exampleuser
            WORDPRESS_DB_PASSWORD: examplepass
            WORDPRESS_DB_NAME: exampledb
        db:
          image: mysql:8.0
          restart: always
          environment:
            MYSQL_DATABASE: exampledb
            MYSQL_USER: exampleuser
            MYSQL_PASSWORD: examplepass
            MYSQL_RANDOM_ROOT_PASSWORD: '1'

- name: Start WordPress containers
  community.docker.docker_compose_v2:
    project_src: /opt/wordpress
    state: present

3. The n8n Role

# roles/n8n/tasks/main.yml
---
- name: Create n8n directory
  ansible.builtin.file:
    path: /opt/n8n
    state: directory

- name: Create docker-compose.yml for n8n
  ansible.builtin.copy:
    dest: /opt/n8n/docker-compose.yml
    content: |
      services:
        n8n:
          image: docker.n8n.io/n8nio/n8n
          restart: always
          ports:
            - "5678:5678"
          volumes:
            - n8n_data:/home/node/.n8n
      volumes:
        n8n_data:

- name: Start n8n container
  community.docker.docker_compose_v2:
    project_src: /opt/n8n
    state: present

Part 4: Deploying Kubernetes (Control Plane & Agent)

Installing Kubernetes requires configuring a base environment on all nodes, initializing the master, and passing a secure token to the worker agents to join.

Create three new roles:

cd roles
ansible-galaxy init k8s_base
ansible-galaxy init k8s_master
ansible-galaxy init k8s_agent
cd ..

1. k8s_base Role (Runs on ALL nodes)

This installs the required K8s software (kubelet, kubeadm, kubectl) and disables swap memory, which K8s requires.

# roles/k8s_base/tasks/main.yml
---
- name: Disable swap immediately
  ansible.builtin.command: swapoff -a
  changed_when: false

- name: Disable swap permanently in /etc/fstab
  ansible.builtin.replace:
    path: /etc/fstab
    regexp: '^([^#].*?\sswap\s+sw\s+.*)$'
    replace: '# \1'

- name: Install required packages
  ansible.builtin.apt:
    pkg: [apt-transport-https, curl]
    state: present

- name: Download K8s GPG key
  ansible.builtin.get_url:
    url: https://pkgs.k8s.io/core:/stable:/v1.30/deb/Release.key
    dest: /tmp/k8s.asc

- name: Dearmor K8s GPG key
  ansible.builtin.command: gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg /tmp/k8s.asc
  args:
    creates: /etc/apt/keyrings/kubernetes-apt-keyring.gpg

- name: Add K8s repository
  ansible.builtin.apt_repository:
    repo: "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.30/deb/ /"
    state: present

- name: Install kubelet, kubeadm, kubectl
  ansible.builtin.apt:
    pkg: [kubelet, kubeadm, kubectl]
    state: present
    update_cache: yes

2. k8s_master Role (Runs ONLY on Master)

This initializes the cluster and saves the join token so Ansible can use it later.

# roles/k8s_master/tasks/main.yml
---
- name: Initialize Kubernetes Control Plane
  ansible.builtin.command: kubeadm init --pod-network-cidr=10.244.0.0/16
  args:
    creates: /etc/kubernetes/admin.conf
  register: kubeadm_init

- name: Create .kube directory
  ansible.builtin.file:
    path: /home/{{ ansible_user }}/.kube
    state: directory
    mode: '0755'
    owner: "{{ ansible_user }}"

- name: Copy admin.conf to user's kube config
  ansible.builtin.copy:
    src: /etc/kubernetes/admin.conf
    dest: /home/{{ ansible_user }}/.kube/config
    remote_src: yes
    owner: "{{ ansible_user }}"

- name: Deploy Flannel Pod Network
  ansible.builtin.command: kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
  environment:
    KUBECONFIG: /etc/kubernetes/admin.conf

- name: Generate join command for agents
  ansible.builtin.command: kubeadm token create --print-join-command
  register: join_command_raw
  changed_when: false

- name: Set join command as a fact (so the agent can read it)
  ansible.builtin.set_fact:
    k8s_join_command: "{{ join_command_raw.stdout }}"

3. k8s_agent Role (Runs ONLY on Agents)

This reads the fact we saved on the master node and executes it to join the cluster.

# roles/k8s_agent/tasks/main.yml
---
- name: Join Kubernetes Cluster
  ansible.builtin.command: "{{ hostvars['192.168.1.100']['k8s_join_command'] }}"
  args:
    creates: /etc/kubernetes/kubelet.conf

Note: Ensure 192.168.1.100 matches the exact name/IP of your master node in your inventory.

Part 5: The Master Playbook (site.yml)

Tie your entire infrastructure together in site.yml:

# site.yml
---
- name: Provision Web Server Apps
  hosts: webservers
  become: yes
  roles:
    - docker
    - wordpress
    - n8n

- name: Provision Kubernetes Base (All Nodes)
  hosts: k8s_cluster
  become: yes
  roles:
    - k8s_base

- name: Initialize Kubernetes Master
  hosts: k8s_master
  become: yes
  roles:
    - k8s_master

- name: Join Kubernetes Agents
  hosts: k8s_agents
  become: yes
  roles:
    - k8s_agent

Run the Full Stack

Execute the master playbook:

ansible-playbook -i inventory.ini site.yml

Ansible will deploy your Docker apps, install K8s software on all nodes, initialize the control plane, grab the secure join token, and use it to attach your worker nodes automatically!

Subscribe to Experiment Lab

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe