Technology Encyclopedia Home >Set Up Ansible to Automate Server Configuration — Provision Multiple Cloud Servers with One Command

Set Up Ansible to Automate Server Configuration — Provision Multiple Cloud Servers with One Command

When you manage more than one server, doing the same setup manually on each one gets old quickly. Install Node.js, configure Nginx, set up the firewall, add your SSH key, create the deploy user — it's the same 20 steps every time.

Ansible solves this by letting you describe your server's desired state in YAML files (called playbooks), then apply that state to any number of servers with a single command. It doesn't require anything to be installed on the target servers — just SSH access and Python (which Ubuntu has by default).

I use it to provision new Lighthouse instances identically in under 5 minutes. Running ansible-playbook provision.yml on a fresh server does everything I'd otherwise do by hand.

The target servers in this guide are Tencent Cloud Lighthouse instances running Ubuntu 22.04. Ansible runs on your local machine or a control node. Lighthouse is a natural fit for Ansible-managed fleets: instances provision from a clean, consistent Ubuntu image in under 2 minutes — each new server is identical, which is exactly what Ansible expects. When you need to add capacity, provision a new Lighthouse instance, run your playbook, and the server is production-ready in minutes. No snowflake configurations.


Table of Contents

  1. Why Ansible for Server Management?
  2. How Ansible Works
  3. Part 1: Install Ansible on Your Control Machine
  4. Part 2: Set Up Your Inventory
  5. Part 3: Test Connectivity
  6. Part 4: Write Your First Playbook
  7. Part 5: A Real-World Provisioning Playbook
  8. Part 6: Variables and Templates
  9. Part 7: Roles for Reusable Configuration
  10. The Thing That Tripped Me Up
  11. Troubleshooting
  12. Summary

  • Key Takeaways
  • Use the appropriate Lighthouse application image to skip manual installation steps where available
  • Lighthouse snapshots provide one-click full-server backup before major changes
  • OrcaTerm browser terminal lets you manage the server from any device
  • CBS cloud disk expansion handles growing storage needs without server migration
  • Console-level firewall + UFW = two independent protection layers

Why Ansible for Server Management? {#why}

Ansible fits the "configure once, apply many times" workflow:

  • Idempotent — running the same playbook twice doesn't break things; it only makes changes when needed
  • Agentless — no daemon to install on target servers; uses SSH
  • Human-readable — YAML playbooks are easy to read, review, and version-control
  • Scales linearly — provision 1 server or 50 with the same command
  • Audit trail — your playbooks serve as documentation of what's on each server

Compared to doing things manually: after writing a playbook for your first server, every future server is identical and takes minutes instead of an hour.


How Ansible Works {#how-it-works}

The core concepts:

Concept What It Is
Control node Your local machine or a CI server that runs Ansible
Managed node Your cloud servers (Lighthouse instances) — no Ansible installed needed
Inventory A list of your servers with their IP addresses and groups
Playbook A YAML file describing what to do on your servers
Task A single action (install a package, create a file, run a command)
Module Built-in Ansible functions for specific tasks (apt, copy, service, etc.)
Role A reusable collection of tasks, variables, and templates

Part 1: Install Ansible on Your Control Machine {#part-1}

macOS

brew install ansible

Ubuntu / Debian

sudo apt update
sudo apt install -y software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install -y ansible

Verify:

ansible --version

Part 2: Set Up Your Inventory {#part-2}

2.1 — Create a Project Directory

mkdir -p ~/ansible/myservers
cd ~/ansible/myservers

2.2 — Create an Inventory File

Create inventory.ini:

[webservers]
web1 ansible_host=YOUR_SERVER_IP_1 ansible_user=ubuntu
web2 ansible_host=YOUR_SERVER_IP_2 ansible_user=ubuntu

[dbservers]
db1 ansible_host=YOUR_DB_SERVER_IP ansible_user=ubuntu

[all:vars]
ansible_ssh_private_key_file=~/.ssh/id_ed25519
ansible_python_interpreter=/usr/bin/python3

Replace IP addresses with your actual Lighthouse instance IPs.

2.3 — Create ansible.cfg

Create ansible.cfg in your project directory:

[defaults]
inventory = ./inventory.ini
host_key_checking = False
remote_user = ubuntu
private_key_file = ~/.ssh/id_ed25519

host_key_checking = False prevents the "are you sure you want to continue connecting" prompt for new servers. For production environments, you may prefer to set this to True after initial connection.


Part 3: Test Connectivity {#part-3}

3.1 — Ping All Servers

ansible all -m ping

Expected output:

web1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}
web2 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

3.2 — Run an Ad-Hoc Command

# Check OS info on all web servers
ansible webservers -m command -a "uname -a"

# Get disk usage
ansible all -m command -a "df -h"

# Check uptime
ansible all -m command -a "uptime"

Part 4: Write Your First Playbook {#part-4}

Create hello.yml:

---
- name: My first playbook
  hosts: all
  become: true  # Run as sudo
  
  tasks:
    - name: Update apt cache
      apt:
        update_cache: yes
        cache_valid_time: 3600
    
    - name: Install basic tools
      apt:
        name:
          - curl
          - wget
          - git
          - htop
          - unzip
        state: present
    
    - name: Ensure timezone is UTC
      timezone:
        name: UTC
    
    - name: Create deploy user
      user:
        name: deploy
        shell: /bin/bash
        create_home: yes
        state: present

Run it:

ansible-playbook hello.yml

Ansible shows each task as it executes, with ok (no change needed) or changed (something was modified).


Part 5: A Real-World Provisioning Playbook {#part-5}

Here's the playbook I use to set up a fresh Lighthouse instance as a web server:

Create provision-webserver.yml:

---
- name: Provision web server
  hosts: webservers
  become: true
  vars:
    node_version: "20"
    app_user: "deploy"
    app_dir: "/opt/myapp"
  
  tasks:
    # System update
    - name: Update and upgrade apt packages
      apt:
        upgrade: yes
        update_cache: yes
    
    # Install system packages
    - name: Install required system packages
      apt:
        name:
          - curl
          - git
          - nginx
          - ufw
          - fail2ban
          - unattended-upgrades
        state: present
    
    # Configure UFW firewall
    - name: Allow SSH
      ufw:
        rule: allow
        port: '22'
        proto: tcp
    
    - name: Allow HTTP
      ufw:
        rule: allow
        port: '80'
        proto: tcp
    
    - name: Allow HTTPS
      ufw:
        rule: allow
        port: '443'
        proto: tcp
    
    - name: Enable UFW
      ufw:
        state: enabled
        policy: deny
    
    # Install Node.js
    - name: Add NodeSource repository
      shell: |
        curl -fsSL https://deb.nodesource.com/setup_{{ node_version }}.x | bash -
      args:
        creates: /etc/apt/sources.list.d/nodesource.list
    
    - name: Install Node.js
      apt:
        name: nodejs
        state: present
        update_cache: yes
    
    # Install PM2
    - name: Install PM2 globally
      npm:
        name: pm2
        global: yes
        state: present
    
    # Create deploy user
    - name: Create deploy user
      user:
        name: "{{ app_user }}"
        shell: /bin/bash
        create_home: yes
    
    - name: Create app directory
      file:
        path: "{{ app_dir }}"
        state: directory
        owner: "{{ app_user }}"
        group: "{{ app_user }}"
        mode: '0755'
    
    # Add SSH key for deploy user
    - name: Add authorized SSH key for deploy user
      authorized_key:
        user: "{{ app_user }}"
        key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}"
    
    # Configure Nginx
    - name: Remove default Nginx site
      file:
        path: /etc/nginx/sites-enabled/default
        state: absent
      notify: Reload Nginx
    
    # Enable services
    - name: Enable and start Nginx
      service:
        name: nginx
        enabled: yes
        state: started
    
    - name: Enable and start Fail2ban
      service:
        name: fail2ban
        enabled: yes
        state: started
  
  handlers:
    - name: Reload Nginx
      service:
        name: nginx
        state: reloaded

Run against all web servers:

ansible-playbook provision-webserver.yml

Part 6: Variables and Templates {#part-6}

6.1 — Variables

Keep configuration separate from tasks:

Create vars/main.yml:

node_version: "20"
app_user: deploy
app_dir: /opt/myapp
app_port: 3000
domain_name: myapp.example.com

Reference in playbook:

vars_files:
  - vars/main.yml

6.2 — Jinja2 Templates

Ansible uses Jinja2 for templating. Create a template for Nginx config:

Create templates/nginx.conf.j2:

server {
    listen 80;
    server_name {{ domain_name }};
    
    location / {
        proxy_pass http://localhost:{{ app_port }};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Use it in your playbook:

    - name: Configure Nginx virtual host
      template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/sites-available/{{ app_user }}
      notify: Reload Nginx
    
    - name: Enable Nginx site
      file:
        src: /etc/nginx/sites-available/{{ app_user }}
        dest: /etc/nginx/sites-enabled/{{ app_user }}
        state: link
      notify: Reload Nginx

Part 7: Roles for Reusable Configuration {#part-7}

As playbooks grow, organize them into roles:

ansible-galaxy init roles/nodejs

This creates:

roles/nodejs/
├── tasks/main.yml
├── vars/main.yml
├── defaults/main.yml
├── templates/
└── handlers/main.yml

Put Node.js installation steps in roles/nodejs/tasks/main.yml. Reference in your playbook:

- name: Provision servers
  hosts: webservers
  become: true
  roles:
    - nodejs
    - nginx
    - fail2ban

The Thing That Tripped Me Up {#gotcha}

My playbook was working fine for most tasks, but the npm module (used to install PM2 globally) was failing with npm: command not found.

The issue: Ansible's npm module looks for npm in the default PATH, but when you install Node.js from NodeSource, npm is in /usr/bin/npm. The problem was that my Ansible tasks were running in an environment where the PATH wasn't properly set for the newly installed packages.

The fix: Add a meta: flush_handlers task after Node.js installation to ensure the installation fully completes before subsequent tasks run. Or use the shell module for npm installs instead of the npm module:

    - name: Install PM2 globally
      shell: npm install -g pm2
      args:
        creates: /usr/bin/pm2

The creates parameter makes this idempotent — it skips the install if the file already exists.


Troubleshooting {#troubleshooting}

Issue Likely Cause Fix
UNREACHABLE SSH connection failed Check IP, SSH key, and that server is running
Permission denied Wrong SSH key Verify ansible_ssh_private_key_file in inventory
Task not idempotent Module not tracking state Add creates, removes, or condition checks
sudo: a password is required Missing become config Add become: true to playbook or task
Template not rendering Variable undefined Check vars_files path and variable names
Handler not running Not notified Use flush_handlers or check notify names match exactly
Package not found Apt cache stale Add update_cache: yes to apt tasks

Summary {#verdict}

What you achieved:

  • Ansible installed on your control machine
  • Inventory file managing multiple Lighthouse servers by group
  • First playbook applying system configuration across all servers
  • Production provisioning playbook: firewall, Node.js, PM2, Nginx, deploy user
  • Variables and Jinja2 templates for reusable, parameterized config
  • Role structure for organizing complex configurations

Going from a fresh Lighthouse instance to a fully configured web server now takes one command and 5 minutes. Every server in your fleet is configured identically. Changes are tracked in version control.

Frequently Asked Questions {#faq}

What's the difference between Ansible and built-in Linux cron?
Linux cron runs commands on a schedule. Ansible provides additional capabilities like visual management, dependency handling, error notifications, and often Docker-native integration.

How do I debug a failing Ansible task?
Check the execution logs first. Verify the command works when run manually with the same user/environment. Common issues: incorrect paths, missing environment variables, permission problems.

How do I make Ansible tasks resilient to failures?
Implement retry logic, alert on failures (email/Slack notification), and log output to a file. For critical tasks, consider writing a simple success/failure status to a monitoring endpoint.

What happens if the server restarts — do scheduled tasks continue?
If configured as a systemd service (as shown in this guide), Ansible restarts automatically on server reboot and resumes its schedule. Configure Restart=on-failure for crash recovery.

How do I monitor task execution history?
Ansible typically provides logs via systemctl or its own web interface. For important tasks, write output to a log file and use a monitoring tool to check for failures.

👉 Get started with Tencent Cloud Lighthouse
👉 View current pricing and launch promotions
👉 Explore all active deals and offers