Automation Guide

    Ansible Deployment Guide

    Ansible is a powerful open-source automation tool that simplifies server configuration and application deployment. Combined with RamNode's reliable VPS hosting, you can automate your entire infrastructure management with ease.

    Ubuntu/Debian/RHEL
    Configuration Management
    ⏱️ 30-45 minutes

    Introduction

    Ansible is an open-source automation tool that simplifies configuration management, application deployment, and task automation. Unlike other configuration management tools, Ansible uses an agentless architecture—requiring only SSH access to manage remote servers. This makes it ideal for managing RamNode VPS instances without installing additional software on your servers.

    This guide will walk you through setting up Ansible on your local machine or control node and demonstrate practical examples for common hosting and server management tasks on RamNode infrastructure.

    Why Use Ansible for RamNode VPS Management?

    Agentless Architecture

    No software installation required on managed nodes

    Idempotent Operations

    Safe to run playbooks multiple times without unintended changes

    Simple YAML Syntax

    Human-readable configuration files that are easy to version control

    Multi-Server Management

    Manage dozens or hundreds of VPS instances simultaneously

    Extensive Module Library

    Over 3,000 built-in modules for common tasks

    Prerequisites

    Before starting, ensure you have:

    • A RamNode VPS (or multiple VPS instances) running Ubuntu 22.04/24.04, AlmaLinux 8/9, or Debian 11/12
    • Root or sudo access to your RamNode VPS
    • SSH key-based authentication configured (recommended)
    • A control machine (your local computer or a dedicated management VPS) with Python 3.8+

    Installing Ansible

    Ubuntu/Debian Control Node

    Install Ansible on 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

    AlmaLinux/Rocky Linux Control Node

    Install Ansible on AlmaLinux/Rocky
    sudo dnf install -y epel-release
    sudo dnf install -y ansible

    macOS Control Node

    Install Ansible on macOS
    brew install ansible

    Python pip (Universal)

    Install via pip
    python3 -m pip install --user ansible

    Verify Installation

    Verify Ansible installation
    ansible --version

    Configuring SSH Access

    For security and convenience, configure SSH key-based authentication:

    1

    Generate SSH Key Pair (if needed)

    Generate SSH key
    ssh-keygen -t ed25519 -C "ansible-control" -f ~/.ssh/ansible_key
    2

    Copy Key to RamNode VPS

    Copy SSH key to server
    ssh-copy-id -i ~/.ssh/ansible_key.pub root@your-ramnode-vps-ip
    3

    Test Connection

    Test SSH connection
    ssh -i ~/.ssh/ansible_key root@your-ramnode-vps-ip

    Basic Ansible Configuration

    Create Ansible Directory Structure

    Create directory structure
    mkdir -p ~/ansible-ramnode/{inventory,playbooks,roles,group_vars,host_vars}
    cd ~/ansible-ramnode

    Create Inventory File

    Create inventory/hosts.ini:

    inventory/hosts.ini
    [webservers]
    web1.ramnode.local ansible_host=192.0.2.10
    web2.ramnode.local ansible_host=192.0.2.11
    
    [databases]
    db1.ramnode.local ansible_host=192.0.2.20
    
    [all:vars]
    ansible_user=root
    ansible_ssh_private_key_file=~/.ssh/ansible_key
    ansible_python_interpreter=/usr/bin/python3

    Create Ansible Configuration File

    Create ansible.cfg:

    ansible.cfg
    [defaults]
    inventory = ./inventory/hosts.ini
    host_key_checking = False
    retry_files_enabled = False
    callback_whitelist = profile_tasks, timer
    
    [privilege_escalation]
    become = True
    become_method = sudo
    become_user = root
    become_ask_pass = False

    Test Connectivity

    Test Ansible connectivity
    ansible all -m ping

    Example 1: Initial Server Hardening

    Create playbooks/server-hardening.yml:

    playbooks/server-hardening.yml
    ---
    - name: Harden RamNode VPS
      hosts: all
      become: yes
      tasks:
        - name: Update all packages
          apt:
            update_cache: yes
            upgrade: dist
            autoremove: yes
          when: ansible_os_family == "Debian"
    
        - name: Update all packages (RHEL)
          dnf:
            name: "*"
            state: latest
          when: ansible_os_family == "RedHat"
    
        - name: Install security packages
          package:
            name:
              - fail2ban
              - ufw
              - unattended-upgrades
            state: present
    
        - name: Configure UFW defaults
          ufw:
            direction: "{{ item.direction }}"
            policy: "{{ item.policy }}"
          loop:
            - { direction: 'incoming', policy: 'deny' }
            - { direction: 'outgoing', policy: 'allow' }
    
        - name: Allow SSH through firewall
          ufw:
            rule: allow
            port: '22'
            proto: tcp
    
        - name: Enable UFW
          ufw:
            state: enabled
    
        - name: Start and enable fail2ban
          systemd:
            name: fail2ban
            state: started
            enabled: yes
    
        - name: Disable root login via SSH
          lineinfile:
            path: /etc/ssh/sshd_config
            regexp: '^PermitRootLogin'
            line: 'PermitRootLogin no'
            state: present
          notify: restart sshd
    
      handlers:
        - name: restart sshd
          systemd:
            name: sshd
            state: restarted

    Run the playbook:

    Run server hardening playbook
    ansible-playbook playbooks/server-hardening.yml

    Example 2: LEMP Stack Deployment

    Deploy Nginx, MySQL, and PHP on your RamNode VPS. Create playbooks/lemp-stack.yml:

    playbooks/lemp-stack.yml
    ---
    - name: Deploy LEMP Stack
      hosts: webservers
      become: yes
      vars:
        mysql_root_password: "SecurePass123!"
        php_version: "8.2"
      tasks:
        - name: Install Nginx
          apt:
            name: nginx
            state: present
            update_cache: yes
    
        - name: Install MySQL Server
          apt:
            name:
              - mysql-server
              - python3-pymysql
            state: present
    
        - name: Start MySQL service
          systemd:
            name: mysql
            state: started
            enabled: yes
    
        - name: Set MySQL root password
          mysql_user:
            name: root
            password: "{{ mysql_root_password }}"
            login_unix_socket: /var/run/mysqld/mysqld.sock
            state: present
    
        - name: Add PHP repository
          apt_repository:
            repo: ppa:ondrej/php
            state: present
    
        - name: Install PHP and extensions
          apt:
            name:
              - php{{ php_version }}-fpm
              - php{{ php_version }}-mysql
              - php{{ php_version }}-curl
              - php{{ php_version }}-gd
              - php{{ php_version }}-mbstring
              - php{{ php_version }}-xml
              - php{{ php_version }}-zip
            state: present
            update_cache: yes
    
        - name: Configure Nginx default site
          template:
            src: templates/nginx-default.j2
            dest: /etc/nginx/sites-available/default
          notify: reload nginx
    
        - name: Start and enable services
          systemd:
            name: "{{ item }}"
            state: started
            enabled: yes
          loop:
            - nginx
            - php{{ php_version }}-fpm
    
        - name: Allow HTTP and HTTPS
          ufw:
            rule: allow
            port: "{{ item }}"
            proto: tcp
          loop:
            - '80'
            - '443'
    
      handlers:
        - name: reload nginx
          systemd:
            name: nginx
            state: reloaded

    Create templates/nginx-default.j2:

    templates/nginx-default.j2
    server {
        listen 80 default_server;
        listen [::]:80 default_server;
        root /var/www/html;
        index index.php index.html index.htm;
        server_name _;
        
        location / {
            try_files $uri $uri/ =404;
        }
        
        location ~ \.php$ {
            include snippets/fastcgi-php.conf;
            fastcgi_pass unix:/var/run/php/php{{ php_version }}-fpm.sock;
        }
        
        location ~ /\.ht {
            deny all;
        }
    }

    Example 3: WordPress Deployment

    Automate WordPress installation. Create playbooks/wordpress.yml:

    playbooks/wordpress.yml
    ---
    - name: Deploy WordPress
      hosts: webservers
      become: yes
      vars:
        wp_domain: "example.com"
        wp_db_name: "wordpress_db"
        wp_db_user: "wp_user"
        wp_db_password: "SecureDBPass123!"
        wp_admin_email: "admin@example.com"
      tasks:
        - name: Download WordPress
          get_url:
            url: https://wordpress.org/latest.tar.gz
            dest: /tmp/wordpress.tar.gz
    
        - name: Extract WordPress
          unarchive:
            src: /tmp/wordpress.tar.gz
            dest: /var/www/
            remote_src: yes
            creates: /var/www/wordpress
    
        - name: Set ownership
          file:
            path: /var/www/wordpress
            owner: www-data
            group: www-data
            recurse: yes
    
        - name: Set directory permissions
          file:
            path: /var/www/wordpress
            mode: '0755'
            recurse: yes
            state: directory
    
        - name: Create WordPress database
          mysql_db:
            name: "{{ wp_db_name }}"
            state: present
            login_unix_socket: /var/run/mysqld/mysqld.sock
    
        - name: Create WordPress DB user
          mysql_user:
            name: "{{ wp_db_user }}"
            password: "{{ wp_db_password }}"
            priv: "{{ wp_db_name }}.*:ALL"
            state: present
            login_unix_socket: /var/run/mysqld/mysqld.sock
    
        - name: Copy wp-config.php
          template:
            src: templates/wp-config.php.j2
            dest: /var/www/wordpress/wp-config.php
            owner: www-data
            group: www-data
            mode: '0640'
    
        - name: Configure Nginx for WordPress
          template:
            src: templates/wordpress-nginx.j2
            dest: /etc/nginx/sites-available/{{ wp_domain }}
          notify: reload nginx
    
        - name: Enable WordPress site
          file:
            src: /etc/nginx/sites-available/{{ wp_domain }}
            dest: /etc/nginx/sites-enabled/{{ wp_domain }}
            state: link
          notify: reload nginx
    
      handlers:
        - name: reload nginx
          systemd:
            name: nginx
            state: reloaded

    Example 4: Docker Installation and Container Deployment

    Install Docker and deploy a containerized application. Create playbooks/docker-deploy.yml:

    playbooks/docker-deploy.yml
    ---
    - name: Install Docker and Deploy Container
      hosts: all
      become: yes
      tasks:
        - name: Install prerequisites
          apt:
            name:
              - apt-transport-https
              - ca-certificates
              - curl
              - gnupg
              - lsb-release
            state: present
            update_cache: yes
    
        - name: Add Docker GPG key
          apt_key:
            url: https://download.docker.com/linux/ubuntu/gpg
            state: present
    
        - name: Add Docker repository
          apt_repository:
            repo: deb [arch=amd64] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable
            state: present
    
        - name: Install Docker
          apt:
            name:
              - docker-ce
              - docker-ce-cli
              - containerd.io
              - docker-compose-plugin
            state: present
            update_cache: yes
    
        - name: Start and enable Docker
          systemd:
            name: docker
            state: started
            enabled: yes
    
        - name: Install Docker Python library
          pip:
            name:
              - docker
              - docker-compose
            state: present
    
        - name: Create application directory
          file:
            path: /opt/myapp
            state: directory
            mode: '0755'
    
        - name: Deploy Nginx container
          docker_container:
            name: nginx-web
            image: nginx:latest
            state: started
            restart_policy: always
            ports:
              - "8080:80"
            volumes:
              - /opt/myapp/html:/usr/share/nginx/html:ro
    
        - name: Create index.html
          copy:
            content: |
              <html>
              <head><title>RamNode VPS</title></head>
              <body>
              <h1>Ansible on RamNode</h1>
              <p>Server: {{ ansible_hostname }}</p>
              </body>
              </html>
            dest: /opt/myapp/html/index.html
            mode: '0644'

    Example 5: Multi-Server Database Replication

    Configure MySQL master-slave replication across multiple RamNode VPS instances. Create playbooks/mysql-replication.yml:

    playbooks/mysql-replication.yml
    ---
    - name: Configure MySQL Master
      hosts: db_master
      become: yes
      vars:
        repl_user: "replicator"
        repl_password: "ReplPass123!"
      tasks:
        - name: Configure MySQL for replication
          lineinfile:
            path: /etc/mysql/mysql.conf.d/mysqld.cnf
            regexp: "{{ item.regexp }}"
            line: "{{ item.line }}"
          loop:
            - { regexp: '^#?server-id', line: 'server-id = 1' }
            - { regexp: '^#?log_bin', line: 'log_bin = /var/log/mysql/mysql-bin.log' }
            - { regexp: '^#?binlog_do_db', line: 'binlog_do_db = production' }
          notify: restart mysql
    
        - name: Create replication user
          mysql_user:
            name: "{{ repl_user }}"
            password: "{{ repl_password }}"
            priv: "*.*:REPLICATION SLAVE"
            host: "%"
            state: present
            login_unix_socket: /var/run/mysqld/mysqld.sock
    
        - name: Get master status
          mysql_replication:
            mode: getmaster
            login_unix_socket: /var/run/mysqld/mysqld.sock
          register: master_status
    
        - name: Save master status
          set_fact:
            master_log_file: "{{ master_status.File }}"
            master_log_pos: "{{ master_status.Position }}"
    
      handlers:
        - name: restart mysql
          systemd:
            name: mysql
            state: restarted
    
    - name: Configure MySQL Slave
      hosts: db_slave
      become: yes
      tasks:
        - name: Configure MySQL for replication
          lineinfile:
            path: /etc/mysql/mysql.conf.d/mysqld.cnf
            regexp: "{{ item.regexp }}"
            line: "{{ item.line }}"
          loop:
            - { regexp: '^#?server-id', line: 'server-id = 2' }
            - { regexp: '^#?relay-log', line: 'relay-log = /var/log/mysql/mysql-relay-bin.log' }
          notify: restart mysql
    
        - name: Configure slave replication
          mysql_replication:
            mode: changeprimary
            primary_host: "{{ hostvars[groups['db_master'][0]]['ansible_host'] }}"
            primary_user: "{{ hostvars[groups['db_master'][0]]['repl_user'] }}"
            primary_password: "{{ hostvars[groups['db_master'][0]]['repl_password'] }}"
            primary_log_file: "{{ hostvars[groups['db_master'][0]]['master_log_file'] }}"
            primary_log_pos: "{{ hostvars[groups['db_master'][0]]['master_log_pos'] }}"
            login_unix_socket: /var/run/mysqld/mysqld.sock
    
        - name: Start slave replication
          mysql_replication:
            mode: startreplica
            login_unix_socket: /var/run/mysqld/mysqld.sock
    
      handlers:
        - name: restart mysql
          systemd:
            name: mysql
            state: restarted

    Example 6: Automated Backups with Restic

    Configure automated backups to Backblaze B2. Create playbooks/backup-setup.yml:

    playbooks/backup-setup.yml
    ---
    - name: Setup Restic Backups
      hosts: all
      become: yes
      vars:
        b2_account_id: "your_account_id"
        b2_application_key: "your_app_key"
        b2_bucket: "ramnode-backups"
        restic_password: "SecureResticPass123!"
        backup_paths:
          - /var/www
          - /etc
          - /home
      tasks:
        - name: Download Restic
          get_url:
            url: https://github.com/restic/restic/releases/download/v0.16.2/restic_0.16.2_linux_amd64.bz2
            dest: /tmp/restic.bz2
    
        - name: Extract Restic
          shell: bunzip2 -c /tmp/restic.bz2 > /usr/local/bin/restic
          args:
            creates: /usr/local/bin/restic
    
        - name: Make Restic executable
          file:
            path: /usr/local/bin/restic
            mode: '0755'
    
        - name: Create Restic environment file
          copy:
            content: |
              export RESTIC_REPOSITORY="b2:{{ b2_bucket }}:/"
              export RESTIC_PASSWORD="{{ restic_password }}"
              export B2_ACCOUNT_ID="{{ b2_account_id }}"
              export B2_ACCOUNT_KEY="{{ b2_application_key }}"
            dest: /root/.restic_env
            mode: '0600'
    
        - name: Initialize Restic repository
          shell: |
            source /root/.restic_env
            restic snapshots || restic init
          args:
            executable: /bin/bash
          register: init_result
          changed_when: "'created restic repository' in init_result.stderr"
          failed_when: false
    
        - name: Create backup script
          copy:
            content: |
              #!/bin/bash
              source /root/.restic_env
              # Run backup
              restic backup {{ backup_paths | join(' ') }} \
                --tag ramnode \
                --exclude-caches \
                --exclude '/var/log/*' \
                --exclude '/tmp/*'
              # Prune old backups
              restic forget \
                --keep-daily 7 \
                --keep-weekly 4 \
                --keep-monthly 6 \
                --prune
              # Check repository
              restic check
            dest: /usr/local/bin/restic-backup.sh
            mode: '0750'
    
        - name: Create daily backup cron job
          cron:
            name: "Restic daily backup"
            hour: "2"
            minute: "0"
            job: "/usr/local/bin/restic-backup.sh >> /var/log/restic-backup.log 2>&1"
            user: root

    Example 7: Monitoring with Prometheus and Grafana

    Deploy a complete monitoring stack. Create playbooks/monitoring.yml:

    playbooks/monitoring.yml
    ---
    - name: Deploy Monitoring Stack
      hosts: monitoring
      become: yes
      tasks:
        - name: Create monitoring directory
          file:
            path: /opt/monitoring
            state: directory
            mode: '0755'
    
        - name: Create docker-compose.yml
          copy:
            content: |
              version: '3.8'
              services:
                prometheus:
                  image: prom/prometheus:latest
                  container_name: prometheus
                  ports:
                    - "9090:9090"
                  volumes:
                    - ./prometheus.yml:/etc/prometheus/prometheus.yml
                    - prometheus-data:/prometheus
                  command:
                    - '--config.file=/etc/prometheus/prometheus.yml'
                    - '--storage.tsdb.path=/prometheus'
                  restart: always
                grafana:
                  image: grafana/grafana:latest
                  container_name: grafana
                  ports:
                    - "3000:3000"
                  volumes:
                    - grafana-data:/var/lib/grafana
                  environment:
                    - GF_SECURITY_ADMIN_PASSWORD=admin
                    - GF_SERVER_ROOT_URL=http://{{ ansible_host }}:3000
                  restart: always
                node-exporter:
                  image: prom/node-exporter:latest
                  container_name: node-exporter
                  ports:
                    - "9100:9100"
                  restart: always
              volumes:
                prometheus-data:
                grafana-data:
            dest: /opt/monitoring/docker-compose.yml
    
        - name: Create Prometheus config
          copy:
            content: |
              global:
                scrape_interval: 15s
                evaluation_interval: 15s
              scrape_configs:
                - job_name: 'prometheus'
                  static_configs:
                    - targets: ['localhost:9090']
                - job_name: 'node-exporter'
                  static_configs:
                    - targets: ['node-exporter:9100']
            dest: /opt/monitoring/prometheus.yml
    
        - name: Start monitoring stack
          docker_compose:
            project_src: /opt/monitoring
            state: present
    
        - name: Allow monitoring ports
          ufw:
            rule: allow
            port: "{{ item }}"
            proto: tcp
          loop:
            - '3000'
            - '9090'
            - '9100'

    Advanced Ansible Patterns

    Using Ansible Vault for Sensitive Data

    Encrypt sensitive variables:

    Using Ansible Vault
    # Create encrypted file
    ansible-vault create group_vars/all/vault.yml
    
    # Add sensitive data:
    vault_mysql_root_password: "SuperSecretPass123!"
    vault_b2_account_key: "your_secret_key"
    
    # Reference in playbooks:
    vars:
      mysql_root_password: "{{ vault_mysql_root_password }}"
    
    # Run with vault password:
    ansible-playbook playbooks/lemp-stack.yml --ask-vault-pass

    Using Roles for Reusability

    Create a reusable role structure:

    Create Ansible roles
    ansible-galaxy init roles/common
    ansible-galaxy init roles/webserver
    ansible-galaxy init roles/database

    Dynamic Inventory

    For managing many RamNode VPS instances, create a dynamic inventory script:

    dynamic_inventory.py
    #!/usr/bin/env python3
    import json
    import sys
    
    def get_inventory():
        inventory = {
            'webservers': {
                'hosts': ['web1.ramnode.local', 'web2.ramnode.local'],
                'vars': {
                    'ansible_user': 'root'
                }
            },
            'databases': {
                'hosts': ['db1.ramnode.local'],
                'vars': {
                    'ansible_user': 'root'
                }
            },
            '_meta': {
                'hostvars': {
                    'web1.ramnode.local': {
                        'ansible_host': '192.0.2.10'
                    },
                    'web2.ramnode.local': {
                        'ansible_host': '192.0.2.11'
                    },
                    'db1.ramnode.local': {
                        'ansible_host': '192.0.2.20'
                    }
                }
            }
        }
        return inventory
    
    if __name__ == '__main__':
        if len(sys.argv) == 2 and sys.argv[1] == '--list':
            print(json.dumps(get_inventory(), indent=2))
        else:
            print(json.dumps({}))

    Best Practices for RamNode Deployments

    • Use Version Control: Store all playbooks in Git for change tracking and collaboration
    • Test in Staging: Always test playbooks on a development VPS before production
    • Use Check Mode: Run playbooks with --check to preview changes without executing
    • Implement Idempotency: Ensure playbooks can run multiple times safely
    • Document Variables: Use clear variable names and maintain a README
    • Tag Your Tasks: Use tags to run specific parts of playbooks
    • Monitor Execution Time: Use callback plugins to identify slow tasks
    • Secure Credentials: Always use Ansible Vault for passwords and API keys

    Troubleshooting Common Issues

    Connection Refused

    If Ansible cannot connect to your RamNode VPS:

    Debug connection issues
    # Test SSH connectivity directly
    ssh -i ~/.ssh/ansible_key root@your-ip -vvv
    
    # Verify inventory
    ansible-inventory --list
    
    # Test with increased verbosity
    ansible all -m ping -vvv

    Python Not Found

    Some minimal OS installations may not include Python:

    Install Python on target hosts
    # Install Python on Ubuntu/Debian
    ansible all -m raw -a "apt update && apt install -y python3" -u root
    
    # Install Python on AlmaLinux/Rocky
    ansible all -m raw -a "dnf install -y python3" -u root

    Permission Denied

    If you encounter permission errors:

    Fix permission issues
    # Check become settings
    ansible-playbook playbook.yml --ask-become-pass
    
    # Or specify become explicitly
    ansible-playbook playbook.yml -b --become-user=root