Ansible Configuration Management 2026: Automate Everything from Servers to Kubernetes

Sanjeev SharmaSanjeev Sharma
7 min read

Advertisement

Ansible 2026: Infrastructure as Code Without the Code

Ansible automates everything from bare metal configuration to Kubernetes deployments. It requires no agents, uses SSH, and its playbooks read like documentation. In 2026, it remains the most pragmatic configuration management tool for teams that need results fast.

Installation and Basic Concepts

# Install Ansible
pip install ansible --break-system-packages
# Or via package manager:
sudo apt install ansible  # Debian/Ubuntu

# Verify
ansible --version

# Quick test: ping all hosts
ansible all -i "server1.com,server2.com," -m ping \
  --user ubuntu --private-key ~/.ssh/id_rsa
# inventory/hosts.ini — Static inventory
[webservers]
web1.webcoderspeed.com ansible_user=ubuntu
web2.webcoderspeed.com ansible_user=ubuntu

[databases]
db1.webcoderspeed.com ansible_user=ubuntu ansible_port=2222

[staging]
staging.webcoderspeed.com ansible_user=ubuntu

[production:children]
webservers
databases

[all:vars]
ansible_python_interpreter=/usr/bin/python3
ansible_ssh_common_args='-o StrictHostKeyChecking=no'
# ansible.cfg — Project configuration
[defaults]
inventory          = ./inventory
remote_user        = ubuntu
private_key_file   = ~/.ssh/id_rsa
host_key_checking  = False
retry_files_enabled = False
stdout_callback    = yaml
gathering          = smart
fact_caching       = jsonfile
fact_caching_connection = /tmp/ansible-facts
fact_caching_timeout = 86400

[privilege_escalation]
become       = True
become_method = sudo

Your First Playbook

# playbooks/setup-webserver.yml
---
- name: Configure web server
  hosts: webservers
  become: true

  vars:
    node_version: "20"
    app_user: "nodeapp"
    app_dir: "/var/www/app"
    nginx_port: 80

  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600

    - name: Install required packages
      apt:
        name:
          - git
          - curl
          - nginx
          - certbot
          - python3-certbot-nginx
        state: present

    - name: Install Node.js via nvm
      shell: |
        curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
        export NVM_DIR="$HOME/.nvm"
        source "$NVM_DIR/nvm.sh"
        nvm install {{ node_version }}
        nvm use {{ node_version }}
        nvm alias default {{ node_version }}
      args:
        creates: "/root/.nvm/versions/node/v{{ node_version }}"  # Skip if exists (idempotent)

    - name: Create app user
      user:
        name: "{{ app_user }}"
        shell: /bin/bash
        system: yes
        create_home: yes

    - name: Create app directory
      file:
        path: "{{ app_dir }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_user }}"
        mode: "0755"

    - name: Copy Nginx config
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/app
        mode: "0644"
      notify: reload nginx

    - name: Enable Nginx site
      file:
        src: /etc/nginx/sites-available/app
        dest: /etc/nginx/sites-enabled/app
        state: link

    - name: Remove default Nginx site
      file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: reload nginx

    - name: Ensure Nginx is running and enabled
      service:
        name: nginx
        state: started
        enabled: yes

  handlers:
    - name: reload nginx
      service:
        name: nginx
        state: reloaded
{# templates/nginx.conf.j2 #}
server {
    listen {{ nginx_port }};
    server_name {{ ansible_fqdn }};

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_cache_bypass $http_upgrade;
    }
}

Roles: Reusable, Structured Automation

Roles are the right way to organize larger automation:

# Create role structure
ansible-galaxy role init roles/nodejs-app

# Generated structure:
# roles/nodejs-app/
# ├── tasks/
# │   └── main.yml
# ├── handlers/
# │   └── main.yml
# ├── templates/
# ├── files/
# ├── vars/
# │   └── main.yml
# ├── defaults/
# │   └── main.yml
# └── meta/
#     └── main.yml
# roles/nodejs-app/tasks/main.yml
---
- name: Install PM2 globally
  npm:
    name: pm2
    global: yes
    state: present

- name: Copy application code
  synchronize:
    src: "{{ local_app_path }}/"
    dest: "{{ app_dir }}/"
    rsync_opts:
      - "--exclude=node_modules"
      - "--exclude=.git"
      - "--exclude=.env"
  become_user: "{{ app_user }}"

- name: Install npm dependencies
  npm:
    path: "{{ app_dir }}"
    production: yes
  become_user: "{{ app_user }}"

- name: Copy environment file
  template:
    src: env.j2
    dest: "{{ app_dir }}/.env"
    owner: "{{ app_user }}"
    mode: "0600"

- name: Copy PM2 ecosystem config
  template:
    src: ecosystem.config.js.j2
    dest: "{{ app_dir }}/ecosystem.config.js"
    owner: "{{ app_user }}"

- name: Start or reload app with PM2
  command: pm2 reload ecosystem.config.js --update-env
  args:
    chdir: "{{ app_dir }}"
  become_user: "{{ app_user }}"
  register: pm2_result
  failed_when: pm2_result.rc != 0

- name: Save PM2 process list
  command: pm2 save
  become_user: "{{ app_user }}"

- name: Setup PM2 startup
  shell: pm2 startup | tail -1 | bash
  when: setup_pm2_startup | default(true)
# roles/nodejs-app/defaults/main.yml
---
app_user: nodeapp
app_dir: /var/www/app
node_env: production
app_port: 3000
pm2_instances: max
setup_pm2_startup: true
local_app_path: "{{ playbook_dir }}/../"
# playbooks/deploy.yml — Use the role
---
- name: Deploy application
  hosts: webservers
  become: true

  vars:
    node_env: production
    app_port: 3000
    database_url: "{{ vault_database_url }}"  # From Ansible Vault

  roles:
    - role: nodejs-app
      vars:
        pm2_instances: 4

Ansible Vault: Encrypted Secrets

# Encrypt a single variable
ansible-vault encrypt_string 'postgresql://...' --name 'vault_database_url'
# Outputs:
# vault_database_url: !vault |
#   $ANSIBLE_VAULT;1.1;AES256
#   ...

# Encrypt a whole file
ansible-vault encrypt group_vars/production/secrets.yml

# Edit encrypted file
ansible-vault edit group_vars/production/secrets.yml

# Run playbook with vault password
ansible-playbook deploy.yml --ask-vault-pass

# Or use a password file (for CI/CD)
echo "my-vault-password" > .vault-pass
chmod 600 .vault-pass
ansible-playbook deploy.yml --vault-password-file .vault-pass
# group_vars/production/secrets.yml (encrypted with vault)
---
vault_database_url: "postgresql://user:pass@db.host/myapp"
vault_redis_url: "redis://host:6379"
vault_jwt_secret: "super-secret-jwt-key"
vault_slack_webhook: "https://hooks.slack.com/services/..."

Dynamic Inventory from AWS

# Install AWS collection
ansible-galaxy collection install amazon.aws

# Configure dynamic inventory
# inventory/aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
  - us-east-1
  - us-west-2

filters:
  tag:Environment: production
  instance-state-name: running

keyed_groups:
  - key: tags.Role
    prefix: role
  - key: placement.availability_zone
    prefix: az

compose:
  ansible_host: public_ip_address
  ansible_user: "'ubuntu'"

# Test dynamic inventory
ansible-inventory -i inventory/aws_ec2.yml --list
ansible-inventory -i inventory/aws_ec2.yml --graph
# Use with playbooks
ansible-playbook -i inventory/aws_ec2.yml deploy.yml \
  --limit role_webserver  # Only hosts with tag Role=webserver

Kubernetes Module: Deploy to K8s with Ansible

# playbooks/k8s-deploy.yml
---
- name: Deploy to Kubernetes
  hosts: localhost
  gather_facts: false
  collections:
    - kubernetes.core

  vars:
    kubeconfig: "~/.kube/config"
    namespace: production
    image_tag: "{{ lookup('env', 'IMAGE_TAG') }}"

  tasks:
    - name: Create namespace
      k8s:
        api_version: v1
        kind: Namespace
        name: "{{ namespace }}"
        state: present

    - name: Create secret from vault variables
      k8s:
        definition:
          apiVersion: v1
          kind: Secret
          metadata:
            name: app-secrets
            namespace: "{{ namespace }}"
          type: Opaque
          stringData:
            DATABASE_URL: "{{ vault_database_url }}"
            REDIS_URL: "{{ vault_redis_url }}"
        state: present

    - name: Deploy application
      k8s:
        definition: "{{ lookup('template', 'k8s/deployment.yml.j2') }}"
        state: present
        wait: yes
        wait_timeout: 120

    - name: Wait for rollout
      k8s_rollout_info:
        name: myapp
        namespace: "{{ namespace }}"
        kind: Deployment
      register: rollout
      until: rollout.conditions | selectattr('type', 'eq', 'Available') | list | length > 0
      retries: 20
      delay: 10

CI/CD Integration: GitHub Actions

# .github/workflows/ansible-deploy.yml
name: Ansible Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install Ansible
        run: pip install ansible boto3

      - name: Install Ansible collections
        run: ansible-galaxy collection install -r requirements.yml

      - name: Setup SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa

      - name: Run Ansible playbook
        env:
          ANSIBLE_HOST_KEY_CHECKING: "False"
          VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
        run: |
          echo "$VAULT_PASSWORD" > .vault-pass
          ansible-playbook \
            -i inventory/aws_ec2.yml \
            playbooks/deploy.yml \
            --vault-password-file .vault-pass \
            -e "image_tag=${{ github.sha }}"
          rm .vault-pass

Useful Ansible Commands

# Dry run (check mode) — no changes made
ansible-playbook deploy.yml --check

# Show diff of what would change
ansible-playbook deploy.yml --check --diff

# Limit to specific hosts
ansible-playbook deploy.yml --limit web1.example.com

# Run specific tags only
ansible-playbook deploy.yml --tags "nginx,app"

# Skip specific tags
ansible-playbook deploy.yml --skip-tags "packages"

# Run step by step (interactive)
ansible-playbook deploy.yml --step

# Increase verbosity
ansible-playbook deploy.yml -v    # verbose
ansible-playbook deploy.yml -vvv  # connection debug
ansible-playbook deploy.yml -vvvv # full debug

# Ad-hoc commands
ansible webservers -m service -a "name=nginx state=restarted" -b
ansible all -m shell -a "df -h"
ansible databases -m setup -a "filter=ansible_memtotal_mb"

Ansible's power is in its simplicity. Playbooks double as documentation. When a new engineer joins the team, reading the playbooks tells them exactly what every server is running and why. That clarity is worth more than any technical feature.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro