MET : Ansible automation (3/6)
Configuring one server manually is acceptable. Configuring ten is tedious. Configuring a hundred is impossible. Ansible solves this problem elegantly.
In the previous article, we saw how Terraform creates our servers. But a freshly created server is “bare”: no Kubernetes, no configuration, nothing. This is where Ansible comes in.
The problem: manual configuration
After creating a server, the task list is long:
- Update the system
- Install dependencies
- Configure the firewall
- Disable swap (required for Kubernetes)
- Load kernel modules
- Install Kubernetes
- Configure networking
- …
Doing this manually poses several problems:
- Time: 2-3 hours per server, minimum
- Errors: One oversight, one typo, and it stops working
- Drift: Over time, servers diverge (different configurations)
- Documentation: “How did we configure that again?”
The solution: Ansible
Ansible is an open-source automation tool that allows you to configure servers in a declarative and idempotent manner.
Idempotent: You can run the same playbook 100 times, the result will always be the same. If something is already configured, Ansible doesn’t redo it.
Key advantages:
- Agentless: Ansible connects via SSH, nothing to install on servers
- Readable: Playbooks are in YAML, understandable by everyone
- Idempotent: Safe and repeatable execution
- Modular: Thousands of modules available
The inventory: defining your servers
Everything starts with the inventory, which lists servers and their characteristics:
# inventory/hosts.yml all: children: k8s_cluster: children: control_plane: hosts: k8s-master: ansible_host: 128.140.xxx.xxx private_ip: 10.0.1.10 workers: hosts: k8s-worker-1: ansible_host: 91.99.xxx.xxx private_ip: 10.0.1.11 vars: ansible_user: root ansible_ssh_private_key_file: ~/.ssh/id_rsa k3s_version: "v1.28.5+k3s1" cluster_cidr: "10.42.0.0/16" service_cidr: "10.43.0.0/16"This inventory defines:
- Groups: control_plane, workers, k8s_cluster
- Per-host variables: Public and private IPs
- Global variables: K3s version, network configuration
Playbook 1: Server preparation
Our first playbook prepares servers for Kubernetes:
# playbooks/prepare-servers.yml --- - name: Server preparation hosts: k8s_cluster become: true # Execute as root tasks: - name: Update packages apt: update_cache: yes cache_valid_time: 3600 - name: Install dependencies apt: name: - curl - wget - git - htop - open-iscsi # Required for Longhorn - nfs-common # Required for NFS storage - cryptsetup # Encryption - jq # JSON parsing state: present - name: Disable swap shell: swapoff -a when: ansible_swaptotal_mb > 0 - name: Permanently disable swap replace: path: /etc/fstab regexp: '^([^#].*?\sswap\s+sw\s+.*)$' replace: '# \1'Each task is explicit:
- name: Readable description of what the task does
- apt: Ansible module for managing Debian/Ubuntu packages
- when: Execution condition (here, only if swap exists)
Kernel configuration
Kubernetes requires certain kernel modules and system parameters:
- name: Configure kernel modules copy: dest: /etc/modules-load.d/k8s.conf content: | overlay br_netfilter ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack - name: Load modules modprobe: name: "{{ item }}" state: present loop: - overlay - br_netfilter - ip_vs - name: Configure sysctl sysctl: name: "{{ item.key }}" value: "{{ item.value }}" state: present reload: yes loop: - { key: 'net.bridge.bridge-nf-call-iptables', value: '1' } - { key: 'net.bridge.bridge-nf-call-ip6tables', value: '1' } - { key: 'net.ipv4.ip_forward', value: '1' } - { key: 'fs.inotify.max_user_watches', value: '524288' }Note the use of loop to repeat a task with different values. This is cleaner than duplicating code.
Playbook 2: Kubernetes (K3s) installation
Once servers are prepared, we install K3s:
# playbooks/install-k3s.yml --- - name: K3s Control Plane installation hosts: control_plane become: true tasks: - name: Download K3s script get_url: url: https://get.k3s.io dest: /tmp/k3s-install.sh mode: '0755' - name: Install K3s server shell: | INSTALL_K3S_VERSION={{ k3s_version }} \ K3S_TOKEN={{ k3s_token }} \ INSTALL_K3S_EXEC="server \ --cluster-init \ --disable traefik \ --write-kubeconfig-mode 644 \ --tls-san {{ ansible_host }} \ --node-ip {{ private_ip }} \ --advertise-address {{ private_ip }} \ --cluster-cidr {{ cluster_cidr }} \ --service-cidr {{ service_cidr }}" \ sh /tmp/k3s-install.sh args: creates: /usr/local/bin/k3s # Only execute if k3s doesn't exist - name: Retrieve kubeconfig fetch: src: /etc/rancher/k3s/k3s.yaml dest: ../kubeconfig flat: yesImportant points:
- creates: Makes the task idempotent (only executes if the file doesn’t exist)
- fetch: Retrieves the kubeconfig to our local machine
- –disable traefik: We’ll install our own configured Traefik
Worker installation
- name: K3s Workers installation hosts: workers become: true tasks: - name: Install K3s agent shell: | INSTALL_K3S_VERSION={{ k3s_version }} \ K3S_URL=https://{{ hostvars['k8s-master']['private_ip'] }}:6443 \ K3S_TOKEN={{ hostvars['k8s-master']['k3s_join_token'] }} \ INSTALL_K3S_EXEC="agent \ --node-ip {{ private_ip }}" \ sh /tmp/k3s-install.sh args: creates: /usr/local/bin/k3sWorkers automatically join the cluster using the control plane’s token.
Playbook 3: Base services
After K3s, we install essential services via Helm:
# playbooks/install-core-services.yml --- - name: Core services installation hosts: control_plane become: true tasks: - name: Install Helm shell: | curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash args: creates: /usr/local/bin/helm - name: Add Helm repos shell: | helm repo add traefik https://traefik.github.io/charts helm repo add longhorn https://charts.longhorn.io helm repo add jetstack https://charts.jetstack.io helm repo add argo https://argoproj.github.io/argo-helm helm repo update environment: KUBECONFIG: /etc/rancher/k3s/k3s.yaml - name: Install Traefik shell: | helm upgrade --install traefik traefik/traefik \ --namespace infra-system \ --create-namespace \ --set ports.web.redirectTo.port=websecure \ --wait --timeout 5m environment: KUBECONFIG: /etc/rancher/k3s/k3s.yaml - name: Install Cert-Manager shell: | helm upgrade --install cert-manager jetstack/cert-manager \ --namespace cert-manager \ --create-namespace \ --set installCRDs=true \ --wait --timeout 5m environment: KUBECONFIG: /etc/rancher/k3s/k3s.yaml - name: Install Longhorn shell: | helm upgrade --install longhorn longhorn/longhorn \ --namespace longhorn-system \ --create-namespace \ --set defaultSettings.defaultReplicaCount=2 \ --wait --timeout 10m environment: KUBECONFIG: /etc/rancher/k3s/k3s.yamlExecuting playbooks
To execute a playbook:
# Test connection to servers ansible all -i inventory/hosts.yml -m ping # Execute a playbook ansible-playbook -i inventory/hosts.yml playbooks/prepare-servers.yml # Dry-run mode (see what would be done without executing) ansible-playbook -i inventory/hosts.yml playbooks/install-k3s.yml --checkIn our project, we simplified with Make commands:
# Test connection make ansible-ping # Install K3s make ansible-install-k3s # Install core services make ansible-core-servicesOur Ansible best practices
📁 Organization
ansible/ ├── inventory/ │ ├── hosts.yml # Server inventory │ └── group_vars/ # Variables per group ├── playbooks/ │ ├── prepare-servers.yml │ ├── install-k3s.yml │ └── install-core-services.yml ├── templates/ # Jinja2 configuration files └── ansible.cfg # Ansible configuration🔐 Security
- Ansible Vault: Encrypt sensitive variables
- SSH keys: Never plaintext passwords
- become: Use sudo rather than direct root
✅ Idempotence
- Use creates or removes for shell commands
- Prefer Ansible modules over shell commands when possible
- Test with –check before applying
The result
With our Ansible playbooks:
| Before (manual) | After (Ansible) |
|---|---|
| 2-3 hours per server | 15 minutes for the entire cluster |
| Separate documentation (often outdated) | The code IS the documentation |
| Different configuration between servers | Identical configuration guaranteed |
| “It worked before…” | 100% reproducible |
What’s next?
We now have:
- ✅ Servers created by Terraform
- ✅ A Kubernetes cluster configured by Ansible
- ✅ Base services installed (Traefik, Longhorn, Cert-Manager)
In the next article, we’ll see how Kubernetes and Rancher allow us to manage our applications with an intuitive interface and powerful features.
🚀 Spending too much time configuring your servers?
We can help you automate your infrastructure with Ansible. Save time, reduce errors, sleep peacefully.