Ansible – SSD Nodes https://www.ssdnodes.com VPS Cloud Hosting For Hundreds Less Sun, 04 Jan 2026 15:49:29 +0000 en-US hourly 1 https://wordpress.org/?v=6.7.1 https://www.ssdnodes.com/wp-content/uploads/2024/09/fav.svg Ansible – SSD Nodes https://www.ssdnodes.com 32 32 Ansible Tutorial for Beginners – A Step By Step Guide https://www.ssdnodes.com/blog/step-by-step-ansible-guide/ https://www.ssdnodes.com/blog/step-by-step-ansible-guide/#respond Fri, 14 Mar 2025 10:00:43 +0000 https://blog.ssdnodes.com/blog/?p=2477 The DevOps field as we know it is changing rapidly, and manually configuring servers isn't just inefficient, it's a dangerous liability that's costing you insane amounts of time. This Ansible step by step tutorial for beginners is your guide to mastering server automation before your competition leaves you behind. Fortune 500 companies are already using Ansible to manage thousands of servers with just a few keystrokes, and after this comprehensive guide, you'll have these same powerful capabilities at your fingertips.

I'll break everything down into bite-sized, actionable steps that you can immediately apply to your infrastructure. By the end of this tutorial, you'll be automating tasks that used to take hours in just minutes; and who knows? This could be the technological edge that transforms your entire operation sooner than you expect.

Notes On This Ansible Tutorial for Beginners

  • You need at least two servers to follow this tutorial, a server for the control host where you install Ansible, and a server for a remote host that will be controlled by Ansible. If you haven't noticed, we offer extremely affordable VPS plans. Check out our website and check out our incredible deals!
  • Whenever you see the SUBDOMAIN, DOMAIN, or TLD variables, replace them with the details of your domain name. In example.ssdnodes.com, the SUBDOMAIN is example, ssdnodes is the DOMAIN and .com is the TLD. Or you can just use an IP address instead of a domain.

Ansible Tutorial for Beginners

Ansible for Beginners - A Simple Definition

Ansible is an open source IT configuration management, deployment, and orchestration tool. It empowers DevOps teams to define their infrastructure as a code in a simple and declarative manner. Imagine being able to set up and control 100 servers as easily as you control one server. That's what Ansible does. It's like a remote control for servers. Instead of doing the same tasks over and over on different machines, you write a simple instruction list once, and Ansible automatically follows those instructions on all your servers at once.

A lot of people compare Ansible to similar tools like Chef or Puppet. They all help automate and provision infrastructure, but there are a few features that make me prefer Ansible over the others.

Step 1: Installing Ansible

To take your first steps with Ansible, you first need to install it on your control machine. This is the machine you’ll use to dispatch tasks. For most people, this will be your desktop machine at home or your laptop, but you can also use one VPS as a control host to connect to other VPSs.

Installing Ansible on Ubuntu 24.04

You can install Ansible using standard package managers like apt/yum or Python’s pip command. To install it using standard package manager in Ubuntu, add its repository information apt-add-repository. Next, update the system and install Ansible using apt-get.

$ sudo apt-get install software-properties-common
$ sudo apt-add-repository ppa:ansible/ansible
$ sudo apt-get update
$ sudo apt-get install ansible

Installing Ansible on AlmaLinux 9

To install Ansible on AlmaLinux 9, update the package list, then install the ansible-core package:

$ sudo dnf update
$ sudo dnf install ansible-core

Step 2: Understanding Ansible Control Host

The control host is the main server on which you've installed Ansible, which you will use to dispatch tasks to the remote managed Ansible hosts.

Before you start delegating tasks to a managed host, make sure you have non-root, a sudo-enabled user on your control host—it’s always a bad idea to connect to a remote VPS via a root user.

Step 3: Ansible Inventory Files

The Ansible inventory file lists which hosts will receive commands from the control host. The inventory can list individual hosts, or group them under categories you distinguish. This file is crucial for defining the managed infrastructure and can include IP addresses or domain names of the hosts. Additionally, you can organize hosts into groups based on various criteria such as their roles, environments (e.g., development, staging, production), or geographic locations.

Ansible Inventory File Location

The default location for the inventory file is /etc/ansible/hosts, but it’s also possible to change the location of the inventory file by uncommenting and modifying the inventory parameter in /etc/ansible/ansible.cfg. This flexibility allows you to maintain multiple inventory files tailored for different environments or projects. For instance, you might have separate inventory files for development, testing, and production environments. Ansible Inventory files are mostly in the INI and YAML formats.

Ansible Inventory File Example

A typical inventory file sample can list the managed host either by IP address or by domain names. It is also possible to list one managed host in more than one group. Here’s an example of listing two hosts under the webservers and dbservers categories.

[webservers]
123.45.67.89
SUBDOMAIN.DOMAIN.TLD

[dbservers]
123.45.67.89
SUBDOMAIN.DOMAIN.TLD

To test if all the hosts are discoverable by the inventory file, use the following ad-hoc command.

$ ansible all --list-hosts

Ansible Tutorial for Beginners: List all Hosts

You can also list the hosts by group name:

$ ansible dbservers --list-hosts
hosts (2):
    123.45.67.89
    SUBDOMAIN.DOMAIN.TLD

Ad-hoc commands in Ansible are merely those that perform a single command across one or many hosts. They don’t use tasks but allow you to do a lot of things quite easily without building out playbooks (more on those in the second part of this guide).

To find out if all the hosts are up and running, use the following ad-hoc command that uses the ping module of Ansible. The -u switch specifies which user Ansible will connect to via SSH—change it according to the non-root user you created earlier.

$ ansible all -m ping -u USER

Ansible Ping

 

The "changed": false in the above JSON result tells us that the ping Ansible task didn’t change anything on the remote server.

Note: You might get an error saying that your host is unreachable. To solve this, simply create an ssh key pair if you don't already have one on your control host:  ssh-keygen -t rsa -b 4096, then copy it to your remote host:  ssh-copy-id USER@REMOTE_HOST_IP.

Rather than specifying all the hosts as in the above command, you can also ping a group of hosts. Specify the group name in place of ‘all’ with the following command:

$ ansible webservers -m ping -u USER

Step 4: Ansible Modules

Modules are the discrete units of code that can be used from the terminal or in a playbook task. They simplify Ansible tasks by installing software, copying files, using templates, and so on.

Modules use the available context to determine what actions if any needed to bring the managed host to the desired state and are idempotent, that means if you run the same task again and again, the state of the machine will not change.

To find the list of available modules, use the following command:

$ ansible-doc -l

Let’s try to install Nginx on an Ubuntu/Debian host using an ad-hoc command in Ansible:

$ ansible webservers -b --become-user=root -m shell -a 'apt -y install nginx' -u USER

172.104.160.8 | SUCCESS | rc=0 >>
Reading package lists...
Building dependency tree...

The following flags were used with the above command:

  • -b: Instruct ansible to become another user to run the command
  • --become-user=root: Run the command as a root user
  • -m: Declares which module is used in the command
  • -a: Declares which arguments are passed to the module

The alternate and preferred way of installing software using an ad-hoc command is to use apt module. If your remote managed host is running RHEL/CentOS, then change the module name from apt to yum.

$ ansible webservers -b --become-user=root -m apt -a 'name=nginx state=present update_cache=true' -u USER

Ansible Modules

 

In the above Ansible command, the -a switch passes the arguments to the apt module by specifying the name of the package to be installed, the desired state, and whether to update the package repository cache or not.

The line change: true in the result section of the above ad-hoc command signifies that the state of the system has been changed. If you run the above ad-hoc command again, the value of changed field will be false, which means the state of the system remains unchanged, because Ansible is aware that Nginx is already present in the system and will not try to alter the state again.

That’s what we call Ansible idempotent. You can run the same ad-hoc command as many times as you’d like and it won’t change anything unless it needs to.

172.104.160.8 | SUCCESS => {
    "cache_update_time": 1530378676,
    "cache_updated": true,
    "changed": false
}

So far, we have understood the ansible modules and its usages through ad-hoc way, but this is not so useful until we use the modules in ansible playbooks to run multiple tasks in the remote managed host.

Step 5: Tasks in Ansible

When you dispatch a job from a control host to a managed host using an Ansible module, it is known as a task. Tasks can be implemented using ad-hoc commands, as we’ve done just above, or you can use an Ansible playbook (more on those in a moment).

One example of a task is copying a file from the control host to a managed host, since it requires the use of ‘copy’ module. There are thousands of modules in Ansible, which means a task can use any of the modules to bring a managed host to the desired state. How many modules are there by default in Ansible? Let’s see:

$ ansible-doc -l | wc -l
1852

If you haven’t guessed, there are a lot of things you can do when combining Ansible tasks and modules.

Step 6: Ansible Playbooks

No Ansible tutorial would be complete without a guide to Playbooks. And some concrete Ansible Playbook examples.

Ansible Playbooks are composed of one or more plays and offer more advanced functionality for sending tasks to managed host compared to running many ad-hoc commands.

The tasks in Ansible playbooks are written in Yet Another Markup Language (YAML), which is easier to understand than a JSON or XML file. Each task in the playbook is executed sequentially for each host in the inventory file before moving on to the next task.

Let’s create a simple Ansible playbook example that will install Nginx and a MySQL server on the managed hosts that we had already defined in the inventory file.

To be more precise, we want Nginx installed on hosts in the webservers group and a MySQL server installed on hosts in the dbservers group.

$ vi playbook.yml

---
- hosts: webservers
  gather_facts: yes
  become_user: root
  tasks:
  - name: Install Nginx
    apt: pkg=nginx state=present
    notify:
    - restart nginx
  - name: Enable Nginx during boot
    service: name=nginx state=started enabled=yes
  handlers:
    - name: restart nginx
      service: name=nginx state=restarted

- hosts: dbservers
  become_user: root
  tasks:
  - name: Install mysql
    apt: pkg=mysql-server state=present

The hosts tells Ansible on which hosts to run the tasks. The above Ansible playbook includes two host groups from the inventory file. The tasks for webservers group are to install Nginx and enable Nginx during boot, and the dbservers group includes a single task to install MySQL.

The become_user in both the host section tells ansible to use sudo to run the tasks.

The gather_facts option gathers information about managed hosts such as distribution, OS family, and more. In ansible terminology, this information is known as FACTS.

The handlers section restarts Nginx when Ansible gets notified that Nginx has been installed.

A handler is the same as a task, but it will be executed when called by another task. It is like an event-driven system. A handler will run a task only when it is called by an event it listens for.

Now run the above playbook example using ansible-playbook. Append the name of the user from a remote managed host in the command using -u switch.

$ ansible-playbook playbook.yml -u USER

Ansible Playbook

The last line contains information about the current run of the above playbook. The four points of data are:

  • ok: The number of tasks that were either executed correctly or didn’t result in a change.
  • changed: The number of things that were modified by Ansible.
  • unreachable: The number of hosts that were unreachable for some reason.
  • failed: The number of tasks failed to execute correctly.

Step 7: Ansible Roles

In Ansible, a role provides a mechanism to break a complicated playbook into multiple reusable components. Each component offers a small function that can be used independently within the playbook. So rather than creating one complex playbook, you can create many roles and simply drop them into your playbooks.

You can’t execute roles directly, the way you do a playbook, and you can’t specify which host you want to execute a role, the way you would an ad-hoc command. Instead, they’re built into the playbooks you use to define a host.

The Ansible Galaxy repository has thousands of pre-built roles for you to choose from, although you’re free to create your role framework. Let’s dig into how you might want to do just that.

Step 8: Ansible Variables

In Ansible, variables are similar to variables in any programming language—they let you input values and numbers dynamically into your playbook. Variables simplify operations by allowing you define and declare them throughout all the various roles and tasks you want to perform.

There are few places where you can define variables in an Ansible playbook.

  • In the playbook
  • In the inventory file
  • In a separate variable file
  • Using group_vars

To define variables in a playbook, use vars key just above the task where you want to use the variable. Once declared, you can use it inside the {{ }} tag. Let’s declare a variable by the name pkgname and assign it the value of the package name that we want to install, which is nginx. Once done, we can use the variable in a task.

---
- hosts: webservers
  gather_facts: yes
  become_user: root

  vars:
    pkgname: nginx

  tasks:
  - name: Install "{{ pkgname }}"
    apt: pkg="{{ pkgname }}" state=present
    ...
    ...

It is also possible to declare a variable in the inventory file using the syntax [host_group_name:vars]. Let’s define the variable pkgname in the inventory file.

[webservers:vars]
pkgname=nginx

Now the variable pkgname can be used anywhere in the webservers hosts section in the playbook.

You can also define variables in a separate variable file and import it into the playbook. Create a variable file using vi another text editor and define the variable pkgname here.

$ vi ansible_vars.yml

---
pkgname: nginx

To use the variable pkgname, import the above file using the vars_files keyword in the playbook.

$ vi playbook.yml

---
- hosts: webservers
  gather_facts: yes
  become_user: root

  vars_files:
    - ./ansible_vars.yml
...
...

Another preferred way of managing variables is to create a group_vars directory inside your Ansible working directory. Ansible will load any YAML files in this directory with the name of any Ansible group.

Create the directory group_vars in your Ansible working directory, and then create the variable files matching with the group name from the inventory file. In our example, this would be webservers and dbservers. This allows you to separate variables according to host groups, which can make everything easier to manage.

$ cd <your_ansible_working_directory>
$ mkdir group_vars
$ cd group_vars
$ vi webservers


Ansible Variables

You don’t need to declare the variable in your playbook, as Ansible will automatically pull the variables from each group_vars files and will substitute them during runtime.

Now suppose you want to have variables that will apply to all the host groups mentioned in the inventory file. To accomplish it, name a file by the name all inside group_vars directory. The group_vars/all files are used to set variables for every host that Ansible connects to.

Step 9: Ansible Conditionals

In Ansible, conditionals are analogous to an if statement in any programming language. You use a conditional when you want to execute a task based on certain conditions. For example, if you only want to install a package on a remote server if it is not already installed, you would use a conditional to check the package's status before proceeding with the installation task.

Ansible Conditionals Example

In our last playbook example, we installed Nginx, so let’s extend that by creating a task that installs Nginx when Apache is not present on the host. We can add another task to the playbook we’ve already built.

...
...
  tasks:
  - name: Check if Apache is already installed
    shell: dpkg -s apache2 | grep Status
    register: apache2_is_installed  
    failed_when: no
  - name: Install "{{ pkgname }}"
    apt: pkg="{{ pkgname }}" state=present
    when: apache2_is_installed.rc == 1
    notify:
    - restart nginx
...
...

The first task in the above playbook checks if Apache is installed using dpkg -s command and stores the output of the task to apache2_is_installed variable. The return value of the task will be a non-zero value if Apache is not installed on the host.

Usually, Ansible would stop executing other tasks because of this non-zero value, but the failed_when: no gives Ansible permission to continue with the next set of tasks when it encounters a non-zero value.

The second task will install Nginx only when the return value of rc is equal to one, which is declared via when: apache2_is_installed.rc == 1.

Step 10: Ansible Loops

All programming languages provide a way to iterate over data to perform some repetitive task. Ansible also provides a way to do the same using a concept called looping, which is supplied by Ansible lookup plugins. With loops, a single task in one playbook can be used to create multiple users, install many packages, and more.

How to use Ansible Loops

While there are many ways to use loops in Ansible, we’ll cover just one of them to get you started. The easiest way to use loops in ansible is to use with_items keyword, which is used to iterate over an item list to perform some repetitive tasks. The following playbook includes a task which installs packages in a loop using the keyword with_items.

---
- hosts: webservers
  gather_facts: yes
  become_user: root

  tasks:

  - name: Installing packages using loops
    apt: pkg={{ item }} state=present update_cache=yes
    with_items:
      - sysstat
      - htop
      - git    

Run the above playbook from your command line, and you’ll see that you’ve installed all three packages on the remote host with a single task!

Step 11: Ansible Tags

Ansible Tags allow you to run only specific tasks from your playbook via the command line. Just add the tags keyword for each task and run only the task(s) that you want by using --tags switch at the end of the ansible command.

How to Use Ansible Tags

In the following playbook, we have added tags at the end of each task, thereby allowing us to run tasks separately from a single playbook.

---
- hosts: webservers
  gather_facts: yes
  become_user: root

  tasks:
  - name: Check if Apache is already installed
    shell: dpkg -s apache2 | grep Status
    register: apache2_is_installed  
    failed_when: no
  - name: Install "{{ pkgname }}"
    apt: pkg="{{ pkgname }}" state=present
    when: apache2_is_installed.rc == 1
    notify:
    - restart nginx
  - name: ensure nginx is running and enable it at boot
    service: name=nginx state=started enabled=yes
    tags:
    - mytag1

  handlers:
    - name: restart nginx
      service: name=nginx state=restarted

- hosts: dbservers
  become_user: root
  tasks:
  - name: Install mysql
    apt: pkg="{{ pkgname }}" state=present
    tags:
    - mytag2

Now run any of the tasks by specifying tag name at the end of ansible command.

$ ansible-playbook playbook.yml -u ansadm --tags 'mytag2'

Step 12: Using Ansible Templates

Typically, after installing a web server like Nginx, you need to configure a virtual hosts file to properly serve a given website on your VPS. Instead of using SSH to log into your VPS to configure it after running Ansible, or using Ansible’s copy module to copy many unique configuration files individually, you can take advantage of Ansible’s templates features.

Ansible Templates

A template file contains all of the configuration parameters you need, such as the Nginx virtual host settings, and uses variables, which are replaced by the appropriate values when the playbook is executed. Template files usually end with the .j2 extension that denotes the Jinja2 templating engine.

To begin working with templates, create a directory for template files in your Ansible working directory.

$ mkdir templates

Create two template files. The first template file will be the default index.html file for each site, and the second template file will contain configuration settings for the Nginx virtual host.

$ cd templates
$ vi index.html.j2
<html>
You are visiting {{ domain_name }} !
</html>

Similarly, create a template file for the Nginx virtual host:

$ vi nginx-vh.j2
server {
        listen       80;
        server_name  {{ domain_name }};
        client_max_body_size 20m;
        index index.php index.html index.htm;
        root   /var/www/html/{{ domain_name }};

        location / {
                    try_files $uri $uri/ /index.html?q=$uri&$args;
        }
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|woff|ttf|svg|otf)$ {
               expires 30d;
               add_header Pragma public;
               add_header Cache-Control "public";
               access_log off;
    }
}

Notice that the variables domain_name in the above two template files are enclosed within {{ }}, which means they will be substituted during runtime by the value of this variable. To define the variable domain_name, navigate to the group_vars directory and edit the file webservers and add the following lines in it.

$ cd group_vars
$ vi webservers

---
domain_name: SUBDOMAIN.DOMAIN.TLD

Finally, edit the ansible playbook to create a root folder for sites, copy the index.html file to the site’s root folder, and copy the virtual host file to the Nginx virtual host directory /etc/nginx/sites-enabled one by one.

$ vi playbook.yml

---
- hosts: webservers
  gather_facts: yes
  become_user: root

  tasks:
  - name: Check if Apache is already installed
    shell: dpkg -s apache2 | grep Status
    register: apache2_is_installed  
    failed_when: no

  - name: Install "{{ pkgname }}"
    apt: pkg="{{ pkgname }}" state=present
    when: apache2_is_installed.rc == 1
    notify:
    - restart nginx

  - name: ensure nginx is running and enable it at boot
    service: name=nginx state=started enabled=yes

  - name: create virtual host root directory
    file: name=/var/www/html/{{ domain_name }} state=directory

  - name: Copying index file to webroot
    template:
      src: templates/index.html.j2
      dest: /var/www/html/{{ domain_name }}/index.html

  - name: Enables nginx virtual host
    template:
      src: templates/nginx-vh.j2
      dest: /etc/nginx/sites-enabled/{{ domain_name }}

  - name: restart nginx
    service: name=nginx state=restarted

    tags:
    - mytag1

  handlers:
    - name: restart nginx
      service: name=nginx state=restarted

- hosts: dbservers
  become_user: root
  tasks:
  - name: Install mysql
    apt: pkg="{{ pkgname }}" state=present
    tags:
    - mytag2

The template task in the above Ansible playbook takes two mandatory parameters src and dest. There are also a few optional parameters that can be specified in a template task but is not required at this stage.

  • The src parameter specifies the name of the template file from templates directory that Ansible will copy to the remote server. In our case, the two templates files that we have created are index.html.j2 and nginx-vh.j2
  • The dest parameter is the path in the remote server where the file should be placed.

Finally, run the playbook from your ansible working directory:

$ ansible-playbook playbook.yml -u USER

Step 13: Ansible Blocks

Ansible blocks, which were introduced in version 2.0, allow you to logically group tasks and better handle errors, which is useful when you want to execute multiple tasks under a single condition. With blocks, you can group a set of tasks together and apply a conditional to the entire block rather than each individual task. This helps in organizing the playbooks and making them more readable.

How to use Ansible Blocks

To end the block, use the when keyword once you’re done defining all the tasks you want to be executed. If the evaluation of the when condition returns true, then all the tasks within the blocks will be executed one by one. All tasks within the blocks will inherit the common data or directives that you set just after the ‘when’ keyword.

---
- hosts: webservers

  tasks:
  - name: Install Nginx

    block:
    - apt: pkg=nginx state=present
    - service: name=nginx state=started enabled=yes

    when: ansible_distribution == 'Ubuntu'
    become: true
    become_user: root

The block section in the above playbook includes two related tasks to install nginx and start/enable it. The when evaluation specifies that these tasks should only be run when the remote managed host is using Ubuntu as its operating system. Both the tasks will inherit the privilege escalation directives after the ‘when’ keyword.

You can also use blocks to handle failures, similar to exceptions in most programming languages. The aim is to gracefully handle failures within the block rather than withdrawing the entire deployment.

Here is an example of how to use blocks to handle failures:

tasks:  
  - block:  

  - name: Enable Nginx during boot
    service: name=nginx state=started enabled=yes

    rescue:  
      - name: This section runs only when there is an error in the block.  
        debug: msg="There was an error in starting/enabling nginx."  
    always:  
      - name: This section will run always.  
        debug: msg="This always executes."`

Conclusion

Okay...so if you followed along with this Ansible tutorial step by step, you’ve gotten a simplified but practical lesson in most how to take your first steps with Ansible:

  • How to install Ansible
  • Running ad-hoc commands
  • Understanding how modules work
  • Creating Ansible playbooks
  • Running your first playbook
  • And a few other key fundamentals to get you started.

More Ansible Tutorials

For more on Ansible, check out the official Ansible documentation.

Check out our secure Ansible playbook tutorial to the complete playbook we put together for securing new VPSs:

]]>
https://www.ssdnodes.com/blog/step-by-step-ansible-guide/feed/ 0
Automated LAMP Setup Using Ansible https://www.ssdnodes.com/blog/ansible-lamp-setup/ https://www.ssdnodes.com/blog/ansible-lamp-setup/#respond Mon, 31 May 2021 19:58:02 +0000 https://blog.ssdnodes.com/blog/?p=5935 Continuing with our Ansible Series, we will move on to a pretty common use case for most VPSes, and that is of LAMP Stack setup. LAMP stands for Linux, Apache, MySQL and PHP.This is a very popular stack of technologies that web developers use to build apps.

Linux refers to the base operating system which in our case will be Ubuntu 20.04 LTS, Apache is the web server that will serve the web content, MySQL (or MariaDB) is the database that will be used to store the state of the app (things like user content, usernames, encrypted passwords, etc) and PHP is the programming language that is used to generate dynamic content. This tech stack is the base for many popular apps like WordPress, Drupal and more.

So let us create an Ansible playbook that will reliably setup LAMP stack on Ubuntu 20.04 LTS. As a developer, this would allow you to experiment and break things, and simply reinstall your VPS and get LAMP up and running with a single ansible command!

Prerequisites

  • A VPS running Ubuntu 20.04 LTS with a public IP. If you don't have one, feel free to pick one up here.
  • Initial Server Setup using Ansible to get you up to speed with ansible, and to harden your server's security.

Installing Packages

Let's start by create a lamp-setup.yaml Ansible Playbook like we did during our initial setup blog, except this time we will use the package builtin module to install the necessary packages:

---
- name: LAMP Setup
  hosts: all
  remote_user: root

  tasks:

  - name: Installing Packages
    package:
      name: "{{ item }}"
      state: present
    with_items:
      - apache2
      - mysql-server
      - php 
      - libapache2-mod-php 
      - php-mysql
      - python3-pymysql

This will install all the basic packages that are needed for a typical LAMP setup. Linux is obviously the base system, mysql-server, apache2, php are pretty self-explanatory as well. We will also need libapache2-mod-php to enable apache2 to talk to the PHP language interpreter. We would also need php-mysql which is a library that allows php to interface with the mysql database. Finally, we will also install python3-pymysql which is not a part of LAMP stack but it is required by Ansible to make MySQL requests on the target system.

Ansible Modules for Setting Up MySQL

Next we need to setup MySQL. This would involve setting up MySQL's root user's password (which is quit different from Linux's root user). Please be very careful and read the following:

  • DO NOT USE the same password as below, rather generate a random, unique and strong password.
  • DO NOT LEAVE the password in your ansible playbook. We will learn about dealing with passwords later in the future.
  • DO NOT PUSH the file .mycnfin your target's root directory to a git repository. This will contain your MySQL credentials and it should be ignored by git and other tools.

Now, that we have that out of the way, we can start using Ansible to configure MySQL. To do this, we will first need to download and install a collection of Ansible modules on our Ansible host machine

$ ansible-galaxy collection install community.mysql

Now we can use the module mysql_user to set root user's password. We will also create a .my.cnf file in the Linux root user's Home directory. This file contains username and password that can be used by Linux's root user to log into MySQL. This will also be used by Ansible in subsequent tasks to automatically authenticate as root user for MySQL without us having to repeatedly supply username and password for each task.

  - name: Set root password
    no_log: true
    community.mysql.mysql_user:
      name: root
      password: PLACE_YOUR_PASSWORD_HERE
      login_unix_socket: /var/run/mysqld/mysqld.sock

  - name: Copy .my.cnf for easier mysql automation
    blockinfile:
      path: ~/.my.cnf
      create: yes
      block: |
        [client]
        user=root
        password="PLACE_YOUR_PASSWORD_HERE"

Notice, we also add no_log: true for each of the tasks above, this is to prevent Ansible from accidentally mentioning the MySQL root password in any of its log files.

Secure MySQL Installation

Most installation of MySQL comes with a default script called mysql_secure_installation which you are meant to run as root. This script prompts you through a list of choices such as setting root password, removing anonymous user and test databases, as well as prohibiting the root user to connect to the the database remotely.

We will use Ansible to carry out these tasks without explicitly calling the script. Since we have already set root password, we only need to prohibit remote root login, remove anonymous user (represented by an empty string '') and we need to delete the test database. The following tasks help us with this:

  - name: Removes test database
    no_log: true
    community.mysql.mysql_db:
      name: test
      state: absent

  - name: Prohibit Remote Root login
    no_log: true
    community.mysql.mysql_query:
      login_db: mysql
      query: "{{ item }}"
    with_items:
      - DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
      - FLUSH PRIVILEGES;

We used different modules above, namely mysql_db and mysql_query. For more information on this you can always refer to their excellent documentation.

Configuring Apache

Next we need to configure Apache to serve php files. By default, Apache is configured to look into the directory /var/www/html directory and serve the index.html file first. If that is not found, it will look for index.htm, followed by a few other extensions.

We will configure it to look for index.php first. After this, you can deploy your PHP application to /var/www/html and Apache will automatically serve it.

  - name: Configure apache2
    lineinfile:
      path: /etc/apache2/mods-enabled/dir.conf
      regexp: "^\tDirectoryIndex index.html index.cgi index.pl index.php index.xhtml index.htm$"
      line: "\tDirectoryIndex index.php index.html index.cgi index.pl index.php index.xhtml index.htm"
    notify: Restart Apache2

  handlers:
      - name: Restart Apache2
        service:
          name: apache2
          state: restarted

The configuration was in file /etc/apache2/mods-enabled/dir.conf and after the task results in a changed state of the file Ansible restarts the Apache web server as specified by notify: Restart Apache2.

The Restart Apache2 handler is defined inside the handlers section.

Final Playbook and Testing PHP rendering

To combine all the above setups, the lamp-setup.yaml playbook would look something like below:

---
- name: LAMP Setup
  hosts: all
  remote_user: root

  tasks:

  - name: Installing Packages
    package:
      name: "{{ item }}"
      state: present
    with_items:
      - apache2
      - mysql-server
      - php 
      - libapache2-mod-php 
      - php-mysql
      - python3-pymysql

  - name: Set root password
    no_log: true
    community.mysql.mysql_user:
      name: root
      password: PLACE_YOUR_PASSWORD_HERE
      login_unix_socket: /var/run/mysqld/mysqld.sock

  - name: Copy .my.cnf for easier mysql automation
    blockinfile:
      path: ~/.my.cnf
      create: yes
      block: |
        [client]
        user=root
        password="PLACE_YOUR_PASSWORD_HERE"

  - name: Removes test database
    no_log: true
    community.mysql.mysql_db:
      name: test
      state: absent

  - name: Prohibit Remote Root login
    no_log: true
    community.mysql.mysql_query:
      login_db: mysql
      query: "{{ item }}"
    with_items:
      - DELETE FROM mysql.user WHERE User='root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
      - FLUSH PRIVILEGES;
  - name: Configure apache2
    lineinfile:
      path: /etc/apache2/mods-enabled/dir.conf
      regexp: "^\tDirectoryIndex index.html index.cgi index.pl index.php index.xhtml index.htm$"
      line: "\tDirectoryIndex index.php index.html index.cgi index.pl index.php index.xhtml index.htm"
    notify: Restart Apache2

  handlers:
      - name: Restart Apache2
        service:
          name: apache2
          state: restarted

To run the playbook use the below commands:

$ ansible-galaxy collection install community.mysql
$ ansible-playbook lamp-setup.yaml

Conclusion

With the playbook handy, you can really accelerate your development workflow. If something breaks in your testing server, you can reinstall the VPS back to a clean state, setup LAMP stack with a single command. If you wish to deploy your application directly from your git repository we have a blog post showing you how to do that as well!

]]>
https://www.ssdnodes.com/blog/ansible-lamp-setup/feed/ 0
Using Ansible Playbooks to setup your server https://www.ssdnodes.com/blog/ansible-initial-setup/ https://www.ssdnodes.com/blog/ansible-initial-setup/#respond Thu, 13 May 2021 18:27:19 +0000 https://blog.ssdnodes.com/blog/?p=5918 If you are new to VM management, it is good to do certain things manually and to learn how the system works. However, once you know the basics you quickly realise there is much more utility in automating the mundane repetitive task. Ansible is the tool that enables us to automate server setup in a flexible, error-resistant way. It has certain benefits over writing your own scripts in POSIX Shell or Bash, and we will get to these benefits over the course of this tutorial.

Prerequisites

  1. A Control Node where Ansible will be installed. This can be your desktop or another VPS. We will be using a VM running Ubuntu 20.04 LTS as our Control Node.
  2. One or more Target or Hosts. We will be using another VM running Ubuntu 20.04 LTS as our Host, which, Ansible will configure for us.
  3. A basic understanding of SSH and how to connect to a remote VPS and use it.

Goals

Before we get into the specific details, it is important to state what we are trying to accomplish, here. The playbook we are about to write will:

  1. Add a public SSH Key for the root user, allowing us to login as the root user using our public-private SSH Key pair. Here's an introduction to SSH and SSH keys
  2. Disable password-based authentication and allow only key-based logins which are much secure.
  3. Update all the packages on the system. Equivalent to running apt update; apt upgrade on Ubuntu or dnf update on CentOS and Fedora.

So let's get started.

Ansible Installation and Basics

On your Control Node, Ansible can be installed using your system's package manager or Python's Package manager pip, since Ansible is written in Python. On macOS, it is recommended that you install it using pip or pip3:

$ pip install -U ansible

On Linux, you can get it straight from your system's package manager:

$ apt install ansible # For Debian or Ubuntu based systems
$ dnf install ansible # For RedHat, Fedora or CentOS based systems

On your Target Host no prior installation is necessary. As long as the Ansible Host has an SSH daemon running and, Python3 installed you are good. All the Linux VMs that you can get on SSDNodes (or any other cloud provider) would readily work with Ansible without any manual intervention. For this reason, Ansible is called an agentless automation engine. Because you don't have to install any specific software on the target.

So now we also know how Ansible works.

  1. It uses SSH to authenticate and take control of a Host, which means using it is as secure as our SSH connection, and we don't have to worry about additional security threats.
  2. It uses Python3 (Python2 works but is deprecated, and not recommended) to run all the automation, checks and data collection on the hosts.

Configuring Ansible

There are three key files needed on the control node:

  1. Ansible Playbook(s) describing what automation to run on your hosts. This will be our main focus.
  2. An inventory file listing all your hosts and grouping them together in logical ways. On most Linux distros this file is /etc/ansible/hosts
  3. Ansible's configuration file. On most Linux distros this file is /etc/ansible/ansible.cfg

For the sake of consistency we would like to have everything, the configuration, the inventory and playbooks, in one folder. So we create a folder called playbooks and create the inventory and configuration files inside it:

$ mkdir playbooks
$ touch ansible.cfg inventory

Ansible will automatically pick the current directory's ansible.cfg file and override the main configuration with this one. Edit the ansible.cfg file and add the following contents to it:

[defaults]
inventory = ./inventory

This will set the current directory's inventory file to be the inventory for our playbooks. Because we are starting small, with just one VPS, we will add just one line to the inventory here, this will be the IP address (or Domain name) of your VPS. Make sure to use your actual IP address and not what is shown below:

127.0.0.1

The offical documentaion shows how you can create more complicated inventory capable of organizing hundreds of servers into dozens of categories.

We are targeting only one server, so we just added that one line here. If you want to save the playbooks to a git repo, make sure that you don't include the inventory file with it, especially if it contains sensitive information such as the IP Addresses of all your servers.

Writing the playbook

A playbook is essentially a description of how you desire the host system to be, also known as the desired state of the system. It is written in YAML, which, if you are unfamiliar, is language similar to JSON or XML but much more human readable while simulatenously being unambigious to a computer program. Think of it as a way of describing and structuring data, rather than writing a set of instructions like in a script or a program.

Create a file called initial-setup.yaml and we can start building our playbook. The following first few lines

---
- name: Initial Setup
  hosts: all
  remote_user: root

The beginning --- describes the start of a YAML file, and is optional. Next, we create a list with each element of the list starting with -. There are going to be lists within lists in our yaml file and this is the outermost list, with just one item in it.

The first element of this outermost list contains an Object (objects are like dictionaries in Python3) and this object has the following attributes:

  • name: Initial Setup which sets the name of our playbook as Initial Setup
  • hosts: all selects which hosts from our inventory file will this playbook act upon. We have decided to act on all the hosts inside the inventory.
  • remote_user: root sets what user do we wish to SSH as into the remote host.

The next item in this Object will be tasks and herein will lie the bulk of our "configurations". I will show the tasks below as part of the larger playbook, because it is important to note that the indentation level on tasks should be same as name. However, tasks itself has a list of, well, tasks within it. Each task is another object. an indentation level below tasks and name. And each task comes with a name and a module along with the action that the module is supposed to take.

---
- name: Initial Setup
  hosts: all
  remote_user: root
  tasks:
  - name: Add SSH key for root
    lineinfile:
      path: ~/.ssh/authorized_keys
      create: yes
      state: present
      line: <COPY YOUR PUBLIC SSH KEY HERE>

So the first task here is named "Add SSH key for root" and it uses an Ansible module lineinfile which makes sure that a specific line is present (or absent) in a given file. Here, the lineinfile module also contains a dictionary of following parameters within it.

  • path: ~/.ssh/authorized_keys specifies which file this particular task is concerned with.
  • create: yes says that, if the file is absent, create it!
  • state: present specifics that we want a given line to be present.
  • line: <COPY YOUR SPECIFIC SSH KEY HERE> specifics what line we want there to be.

Different modules will have different parameters. For example, package module will not have a path or line variables because the installation of a package has nothing to do with path or lines.

Obviously, no one can remember how each module works and what parameters it takes. So, when you are writing your playbooks, the ansible documentation will be your best guide. For example, the lineinfile module has all its various parameters described, along with the default values, and examples, here. The documentation is clean, easy to follow, and full of relevant examples.

You can see in the docs that state: present is already the default value. So you can skip that line from your playbook if you just want to ensure that the given line exists in the file.

Ansible's automation system has hundreds of built-in modules, and you can also access many more community provided modules from ansible-galaxy.

Running the playbook

To run the above playbook, switch to the directory where the playbook lives and use the ansible-playbook command:

$ cd playbooks/
$ ansible-playbook initial-setup.yaml

If you have not set your SSH keys, and are going to login using password, install sshpass on your local machine and use ansible-playbook with the flag --ask-pass to allow ansible to login using plain text password:

$ sudo apt install sshpass
$ ansible-playbook --ask-pass initial-setup.yaml

Enter the password for your VPS when prompted.

The above command will be required only for the first time, since we are adding SSH keys as part of our server configuration.

Idempotent

Why not write a bash script to automate something like that? Well, bash scripts don't have the same reliability as Ansible. If there is a bug in your bash script, even if it is the most minor of bugs, it can potentially clobber your VPS and can lock you out of it, or corrupt its data.

Where as with Ansible it is much harder to unintentionally clobber the system. For example, if you run the above playbook again, it won't add the same SSH key twice to your authorized_keys file. But if you just do a cat keyfile.pub >> authorized_keys in bash, it will keep adding the line again and again each time you run the script. With more complicated setups a home made bash script won't be able check for errors, edge cases, or stay idempotent. Idempotent operations are those that can occur on a system one or more times and not change the system. It is only the first run that would matter.

So if your inventory grows, or if you add a new task to your playbook, you can simply rerun the playbook, and not worry about the pre-existing tasks, or hosts being affected.

Adding more stuff to our playbook and using Conditionals

Let's get the ball rolling with a slightly more complicated task. To upgrade all the system packages.

For Debian/Ubuntu hosts we will use the apt module.

  - name: Update all packages for Debian-like Systems
    apt:
      update_cache: yes
      name: '*'
      force_apt_get: yes
      state: latest

Consult the documentation for the apt module to understand various parameters here.
Obviously, the same module won't work on CentOS, RedHat and Fedora like systems, so we need to ensure that this works only for the Debian family of distributions. To do that, we will use the fact that ansible gathers certain facts about the host, like what operating system it is running, what packages index it has, etc.

We can use this information to create a condition statement like so:

  - name: Update all packages for Debian-like Systems
    apt:
      update_cache: yes
      name: '*'
      force_apt_get: yes
      state: latest
    when: ansible_facts['os_family'] == 'Debian'

The when needs to be at the same indentation level as apt and name. This will ensure that this task is skipped for CentOS, RedHat and Fedora like systems. Let's write another module to update packages on those systems.

  - name: Update all packages for RHEL like systems
    dnf:
      update_cache: yes
      name: "*"
      state: latest
    when: ansible_facts['os_family'] == 'RedHat'

Looping through a list of items in a task

The above two tasks of updating and upgrading the systems are the only ones that are OS specific. Ansible has a generic module called package that can be used to install packages on top of most pacakge managers. So we can use this to install a whole list of packages now:

  - name: Install Packages
    package:
      name: "{{packages}}"
      state: present
    vars:
      packages:
        - vim
        - curl
        - wget
        - nginx

Notice, we did something more intricate here. Instead of rewriting a list of tasks, all of which uses package module, like below:

  - name: Install vim
    package:
      name: vim
      state: present

We instead created a list of packages that we need, and we looped over them. This is the power of Ansible and YAML. The little bits and pieces that we learned earlier, like lists, and objects are now used to extend really simple tasks. The vars keyword is special to Ansible and is used to declare variables, in this case the variable is called packages which itself has a list of package names inside it. Ansible sees the variable name inside curly braces like this {{packages}} and understands to loop over the list of items.

There are other ways of looping as well using keywords loop and with_items.

Editing Configuration file

We initially promised that we will automatically configure SSH to accept only keys and not plain text passwords. To do this we will again use lineinfile module to rewrite our /etc/ssh/sshd_config file and this time we will add a handler (which is another concept in Ansible) that will restart the SSHD service everytime the configuration file changes. Remember, that ansible playbooks are idempotent, so it doesn't mean that the SSHD service will restart everytime you run the playbook.

 - name: Configure SSH Daemon
    lineinfile:
      path: /etc/ssh/sshd_config
      regexp: '^[(#)|(# )]?PasswordAuthentication [(yes)|(no)]+$'
      line: 'PasswordAuthentication no'
   notify: Restart SSHD

  handlers:
    - name: Restart SSHD
      service:
        name: sshd
        state: restarted

This needs a bit of explanation. The lineinfile module, here, searches for a given regexp (a regular expression) which is essentially a pattern. Here, the pattern says look for a line that may start with # or the same symbol followed with a whitespace, or nothing, followed by PasswordAuthentication, followed by a space and then either a yes or a no followed by end of line, described by +$. This may include the following lines:

  • #PasswordAuthentication yes
  • # PasswordAuthentication yes
  • PasswordAuthentication yes

And the same three patterns are repeated with no at the end.

Once such a pattern is found, the lineinfile module replaces it with PasswordAuthentication no and then, if the state of the file changes, it notifies the handler Restart SSHD to restart the sshd service so that the new configuration takes into affect. If the pattern is not found, the line is added at the end of the file.

If all of this seems a bit heavy, just go through the documentation. You don't need regular expressions to work with ansible. It is only a small part of it. But if you are interested in knowing more here is a really great video on the topic.

Conclusion

Here is a complete playbook for you to run, with different syntax for loops illustrated, and with a few additional handlers to clean up unused old packages from the system. Be sure to add your public ssh key to be appropriate spot.

---
- name: Initial Setup
  hosts: all
  remote_user: root

  tasks:
  - name: Add SSH key for root
    lineinfile:
      path: ~/.ssh/authorized_keys
      create: yes
      state: present
      line: <YOUR PUBLIC SSH KEY HERE>

  - name: Configure SSH Daemon
    lineinfile:
      path: /etc/ssh/sshd_config
      regexp: "{{ item.regexp }}"
      line: "{{ item.line }}"
    with_items:
       - { regexp: '^[(#)|(# )]?Port[ 0-9]+$', line: 'Port 22' }
       - { regexp: '^[(#)|(# )]?PermitRootLogin [(yes)|(no)|(without\-password)]+$' , line: 'PermitRootLogin without-password' }
       - { regexp: '^[(#)|(# )]?PasswordAuthentication [(yes)|(no)]+$', line: 'PasswordAuthentication no' }
    notify: Restart SSHD

  - name: Update all packages for Debian-like Systems
    apt:
      update_cache: yes
      name: '*'
      force_apt_get: yes
      state: latest
    when: ansible_facts['os_family'] == 'Debian'
    notify: Autoremove Packages using APT

  - name: Update all packages for RHEL like systems
    dnf:
      update_cache: yes
      name: "*"
      state: latest
    when: ansible_facts['os_family'] == 'RedHat'
    notify: Autoremove Packages using DNF

  - name: Install Packages
    package:
      name: "{{packages}}"
      state: present
    vars:
      packages:
        - vim
        - curl
        - wget

  handlers:
    - name: Autoremove Packages using DNF
      dnf:
        autoremove: yes

    - name: Autoremove Packages using APT
      apt:
        autoremove: yes

    - name: Restart SSHD
      service:
        name: sshd
        state: restarted

Sign up for our newsletter, as there are more Ansible tutorials to automate Docker, LAMP, LEMP and MEAN setup. And I hope that this playbook will save you a lot of time and hassle in the future!

]]>
https://www.ssdnodes.com/blog/ansible-initial-setup/feed/ 0
Getting started with Ansible for configuration management (Tutorial) https://www.ssdnodes.com/blog/ansible-tutorial-getting-started/ https://www.ssdnodes.com/blog/ansible-tutorial-getting-started/#respond Wed, 24 Apr 2019 00:00:00 +0000 http://ssdnodes.billabailey.com/2017/04/24/tutorial-getting-started-with-ansible-and-configuration-management/ In this Ansible tutorial for beginners, we’ll cover getting started with Ansible as a configuration management tool for setting up a bare CentOS, Debian, and Ubuntu server with more secure SSH settings and a few tools to make your life a little easier.

Our goals:

  1. Set up a non-root user
  2. Give the new user sudo access
  3. Disable password-based logins
  4. Disable root logins
  5. Use SSH keys for logins

Prerequisites for this Ansible configuration management tutorial

  • A newly-provisioned or rebuilt server running any of our OS options—CentOS, Debian, or Ubuntu.

Step 1: Install Ansible on your local machine

To get started using Ansible for configuration management, you first need to install it on your local machine. Ansible's documenation gives installation instructions for a variety of platforms, including various *nix distributions and OS X.

Step 2: Edit the Ansible hosts file

To connect Ansible to your VPS, you need to specify its IP address within Ansible’s hosts file. On Linux and OS X machines, that can be found at /etc/ansible/hosts.

The beginning of the file should look like this:

# This is the default ansible 'hosts' file.
#
# It should live in /etc/ansible/hosts
#
#   - Comments begin with the '#' character
#   - Blank lines are ignored
#   - Groups of hosts are delimited by [header] elements
#   - You can enter hostnames or ip addresses
#   - A hostname/ip can be a member of multiple groups

# Ex 1: Ungrouped hosts, specify before any group headers.

## green.example.com
## blue.example.com
## 192.168.100.1
## 192.168.100.10

To enable your VPS, simply add the IP address anywhere in this file underneath an [ssdnodes] grouping.

There should be no other symbols—like the # comment—in the line.

[ssdnodes]
123.45.67.89

Now, test out your configuration by pinging your VPS. For now, you have to use -u root to ensure you’re trying to connect via the root account.

$ ansible all -m ping -u root

If it’s successful, you’ll see the following output:

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

Step 3: Getting started with Ansible playbooks

To get started using Ansible to manage server configurations, we need to create an Ansible playbook. A playbook is the core component of any Ansible configuration.

The playbook will define the tasks that need to be completed to configure your servers. The ability to create and run Playbooks is the key reason that it's so powerful to use Ansible for configuration management.

The Ansible playbook is in the common .yaml language.

$ mkdir ansible && cd ansible
$ touch create_user.yaml
$ nano create_user.yaml

And here is a basic playbook example that accomplishes our goals.

Note: This playbook is meant to run on a bare CentOS 7 server. If you want to run this on an Ubuntu/Debian server, simply change the yum line to apt.

---
- hosts: ssdnodes
  remote_user: root

  vars_prompt:

    - name: "user_name"
      prompt: "Enter a name for the new user"
      private: no
      confirm: yes

    - name: "user_password"
      prompt: "Enter a password for the new user"
      private: yes
      encrypt: "sha512_crypt"
      confirm: yes
      salt_size: 7

  tasks:

    - name: Check to make sure we have a 'wheel' group
      group:
        name: wheel
        state: present

    - name: Install the 'sudo' package
      yum:
        name: sudo
        state: latest

    - name: Create the non-root user
      user:
        name: ""
        password: ""
        shell: "/bin/bash"
        groups: "wheel"

    - name: Add local public key for key-based SSH authentication
      authorized_key:
        user: ""
        key: "{{item}}"
      with_file:
        - ~/.ssh/id_rsa.pub

    - name: Restrict root SSH logins
      lineinfile:
        dest: /etc/ssh/sshd_config
        state: present
        regexp: '^#PermitRootLogin'
        line: 'PermitRootLogin no'

    - name: Restrict SSH logins to keys only
      lineinfile:
        dest: /etc/ssh/sshd_config
        state: present
        regexp: '^#PasswordAuthentication'
        line: 'PasswordAuthentication no'

    - name: Restart sshd
      systemd:
        state: restarted
        daemon_reload: yes
        name: sshd

Before we go into how you run this command, let’s walk through what some of these lines do in practice.

- hosts: ssdnodes
  remote_user: root

These two lines dictate which host group we’re going to work with—in this case, the ssdnodes group we created earlier—and specify that we’re using the root login (just this once) to complete our steps.

vars_prompt:

  - name: "user_name"
    prompt: "Enter a name for the new user"
    private: no
    confirm: yes

  - name: "user_password"
    prompt: "Enter a password for the new user"
    private: yes
    encrypt: "sha512_crypt"
    confirm: yes
    salt_size: 7

These two vars_prompt commands will ask for user input to define which username and password they would like to associate with the newly-created account.

Beyond this, each nested block of script that begins with - name: defines a new task that Ansible will complete in sequential order, once the previous task has completed successfully. Failed tasks will cause the entire playbook to stop running.

If you follow along with each of the tasks, you can see that we’re installing sudo, creating our new user, adding your SSH public key to the server, and putting some basic restrictions on sshd before restarting it.

Step 4: Run the Ansible playbook

Running this Ansible playbook is fairly straightforward. Here’s the command we’ll use:

ansible-playbook create_user.yaml --ask-pass

We need to include --ask-pass so that Ansible uses a password to log into the server rather than try to use an SSH key that isn’t there.

Once you run the command, you’ll be asked to enter the SSH password:. This is the root login for your server—that password can be found in your SSD Nodes dashboard.

Once you’ve entered the root password, you’ll be prompted to specify and confirm a username and password. Once that’s done, Ansible will get to work!

With any luck, Ansible runs smoothly, and you'll see the following in your terminal:

 ____________
< PLAY RECAP >
 ------------
           ^__^
           (oo)_______
            (__)       )/
                ||----w |
                ||     ||

123.45.67.89               : ok=8    changed=6    unreachable=0    failed=0  

At this point, you’ll be able to log into your new user account using your SSH key.

More resources on Ansible for configuration management:

You’re now ready to get started using Ansible to manage the configuration of new servers with ease, and with an eye toward security.

For a deeper dive into getting started with Ansible, including in-depth explanations of all the components and terminology, check out our "Step by step guide to Ansible" tutorial.

To get more concrete playbook examples aimed toward maximizing security-- to help you harden your SSH, fend off brute force attacks, and more-- check out our 2-part series:

Ansible playbook for a more secure VPS (part 1)
A More Secure Ansible Playbook (Part 2)

And finally, for more information about how to use Ansible for automated server hardening, check out one these resources:

[cta text2="You're 90 seconds away from running Ansible on an SSD Nodes cloud server!" button="Ansible all the things!"]

]]>
https://www.ssdnodes.com/blog/ansible-tutorial-getting-started/feed/ 0
Ansible security playbook for your VPS (part 1) https://www.ssdnodes.com/blog/secure-ansible-playbook/ https://www.ssdnodes.com/blog/secure-ansible-playbook/#respond Wed, 31 Oct 2018 00:00:00 +0000 http://ssdnodes.billabailey.com/2017/08/31/tutorial-a-more-secure-ansible-playbook-part-1/ In our last Ansible tutorial, we covered the basics of using Ansible for configuration management, which can help you get new servers set up faster and more reliably.

But the Ansible security playbook that we created there was pretty basic, so I thought we would show create a new playbook that supports more security out of the box without sacrificing normal access to the server.

The goals for this Ansible security playbook are:

  • Set up a non-root user with sudo access
  • Upgrade all installed packages
  • Install a few basic packages to make initial management easier, like nano. These can be easily customized according to your needs
  • Copy your SSH key to the VPS to enable password-less logins
  • Harden SSH with some basic security measures, such as disabling root and password-based logins
  • Install iptables if needed, and set up some basic restrictions to improve security
  • Install fail2ban to help prevent brute force attacks

(The final two steps will be outlined in part 2 of this Ansible security tutorial).

The goal here isn't to have you copy the code here and recreate your own playbook-- it's to teach you how to do it for yourself.

We'll walk you through the various components step-by-step so that you can use this playbook as a foundation for your own customizations.

And this playbook isn't comprehensive when it comes to security for your VPS—once you provision the server using this playbook, you may want to research some additional steps, such as using Lynis to audit your security.

Prerequisites

  1. A newly-provisioned or rebuilt server running any of our OS options—CentOS, Debian, or Ubuntu.
  2. Ansible installed on your local machine— see these instructions for more details
  3. An Ansible hosts file set up with the IP(s) of your server(s)—see Step 2 of our previous tutorial

Step 1. Setting up the playbook structure

Ansible playbooks can be structured in a number of different ways, but the developers do have their recommendations.

This Ansible script is still relatively simple compared to what's possible with the system, so our structure is going to be far simpler as well.

Here's the general structure we're following:

provision.yml

roles
  common/
    tasks/
      main.yml
  ssh
    tasks/
      main.yml
  packages
    tasks/
      main.yml
  iptables
    tasks/
      main.yml

If you want, you can go ahead and create the directories now, just to give you a better sense as to how the playbook separates its logic into different areas.

Step 2. Creating provision.yml

The provision.yml file is the core of our playbook—it's where we define which servers we're going to be working with, a few global variables, and tell Ansible where to look for its tasks.

---
- name: Provision a new server with hardened SSH and basic iptables.

  # Specify the hosts you want to target
  hosts: HOSTNAME

  # Specify the user you want to connect to the server.
  # With a new installation, you will connect with root. If you want to
  # re-run this playbook at a later date, you should change remote_user to
  # the user you specified under vars/username below and uncomment the
  # become: true line. You should then run the playbook using the
  # --ask-become-pass flag, like so:
  # ansible-playbook -k provision.yml --ask-become-pass.
  remote_user: root
  # become: true

  vars:
    username: USER
    # Before first using the playbook, run the below command to create a hashed
    # password that Ansible will assign to your new user.
    # python -c 'import crypt; print crypt.crypt("<b>password</b>", "$1$<b>SALT</b>$")'
    password: PASSWORD
    public_key: ~/.ssh/id_rsa.pub

  roles:
    - user
    - packages
    - ssh
    - iptables

There are a number of variables that you will need to change according to your needs.

Step 3. Creating user/tasks/main.yml

Our first major step is setting up the right environment for a new non-root user, and then creating that user. Here's the first component:

- name: Ensure wheel group is present
  group:
    name: wheel
    state: present

This Ansible task is a simple one: it checks to see if the wheel group exists on your server. If it doesn't for some reason—it should on all our OS options—the playbook will fail, and then you can fix it with the groupadd command.

The next step is a critical one, so let's take a look:

- name: Ensure wheel group has sudo privileges
  lineinfile:
    dest: /etc/sudoers
    state: present
    regexp: "^%wheel"
    line: "%wheel ALL=(ALL:ALL) ALL"
    validate: "/usr/sbin/visudo -cf %s"

This is an example of using regex to replace one line within a file with a different string of text.

We're looking inside of the /etc/sudoers file, and requesting a line that begins (^) with %wheel. When that line is found, we replace the entire line with %wheel ALL=(ALL:ALL) ALL, which allows users in the wheel group to execute commands using sudo.

When it comes to editing /etc/sudoers, the final validate line is critical, as you would rather the playbook fail due to an improper file than break your administrator capabilities.

We want to make sure the sudo package is installed as well.

- name: Install the sudo package
  package:
    name: sudo
    state: latest

Installing any package, whether it's for CentOS, Ubuntu, or Debian, works this exact same way. That's the beauty of Ansible—you can create one task that works anywhere due to the built-in logic.

Finally, we create the non-root user account that was specified in the variables in provision.yml.

- name: Create the non-root user account
  user:
    name: ""
    password: ""
    shell: /bin/bash
    update_password: on_create
    groups: wheel
    append: yes

This tasks sets up the user with the hashed password you created, and sets the shell to /bin/bash. Because we're putting this user in the wheel group, we'll be able to use sudo straightaway.

Step 4. Creating packages/tasks/main.yml

The packages task is really simple: we just want to update all packages so that we have the latest in security fixes, and then install any number of extra packages according to our specific needs.

- name: Upgrading all packages (Ubuntu/Debian)
  apt:
    upgrade: dist
  when: ansible_os_family == "Debian" or ansible_os_family == "Ubuntu"

- name: Upgrading all packages (CentOS)
  yum:
    name: '*'
    state: latest
  when: ansible_os_family == "RedHat"

The key to these two tasks is the when option—this allows you specify when to run certain commands depending on the OS you've chosen. This is necessary, because yum won't work on Ubuntu, and apt won't work on CentOS.

In either case, we're simply asking the respective package manager to update every installed package.

We can also install additional packages:

- name: Install a few more packages
  package:
    name: "{{item}}"
    state: installed
  with_items:
   - vim
   - htop

Essentially, we're asking the package task to look through the list of items under with_items and install each of them in sequence. If you want some of your own packages, just customize that list to your heart's content.

Step 5. Creating ssh/tasks/main.yml

Next up, we want to enable logging into the newly-created user with SSH keys rather than passwords—a simple-but-effective VPS security measure.

Beyond that, we want to use Ansible to make some configuration changes to the SSH daemon that will harden it against some basic attacks. It's not foolproof, but it's a big step above the defaults.

- name: Add local public key for key-based SSH authentication
  authorized_key:
    user: ""
    state: present
    key: ""

This command looks for an SSH key on the local machine at the location specified in the vars section in provision.yml and then copies it to the server. Much easier than using ssh-copy-id, eh?

Next, let's make SSH a little more secure.

- name: Harden sshd configuration
  lineinfile:
    dest: /etc/ssh/sshd_config
    regexp: "{{item.regexp}}"
    line: "{{item.line}}"
    state: present
  with_items:
    - regexp: "^#?PermitRootLogin"
      line: "PermitRootLogin no"
    - regexp: "^^#?PasswordAuthentication"
      line: "PasswordAuthentication no"
    - regexp: "^#?AllowAgentForwarding"
      line: "AllowAgentForwarding no"
    - regexp: "^#?AllowTcpForwarding"
      line: "AllowTcpForwarding no"
    - regexp: "^#?MaxAuthTries"
      line: "MaxAuthTries 2"
    - regexp: "^#?MaxSessions"
      line: "MaxSessions 2"
    - regexp: "^#?TCPKeepAlive"
      line: "TCPKeepAlive no"
    - regexp: "^#?UseDNS"
      line: "UseDNS no"
    - regexp: "^#?AllowAgentForwarding"
      line: "AllowAgentForwarding no"

The lineinfile and regexp should look familiar to you at this point—as with making changes to /etc/sudoers, we're looking at /etc/ssh/sshd_config and replacing a number of existing lines with new ones. If the lines don't currently exist, Ansible will create new lines at the bottom of the file containing our revisions. The ^#? regex allows us to replace lines whether or not they're commented out, and thus begin with a #.

Finally, let's have the SSD daemon to make sure our changes are applied.

- name: Restart sshd
  systemd:
    state: restarted
    daemon_reload: yes
    name: sshd

This systemd task allows us to run the equivalent of systemd restart sshd.

Final thoughts

As mentioned above, we're hitting pause here for a moment and will return a week from now with the second half of this Ansible security playbook tutorial, which will walk through a basic iptables configuration, and install fail2ban.

But, in the meantime, you can run this playbook now and later.

That's the great thing about a correctly-configured Ansible playbook— they are idempotent, which means they can be run again and again without changing the result beyond the initial installation. You can run the playbook once, make a small change such as adding another package to be installed under the packages role, and then run the playbook again without error.

Once you get everything up and running, how do you actually run this playbook? It's pretty straightforward.

Generate a hashed password. You first need to convert the password you want for your non-root user into a hashed password. This command should work on Linux and OS X, and be sure to replace password with your chosen password: python -c 'import crypt; print crypt.crypt("password", "$1$AnsibleSalt$")'.

Copy your new hash into provision.yml.

Run Ansible. Running the playbook itself is straightforward, with one simple command: $ ansible-playbook -k provision.yml

If you need to re-run the playbook after the first run, you'll need to make some changes to provision.yml—look at the code above (and in the version you've created) for some basic instructions.

Stay tuned for next week's conclusion, along with the full code you need to run this playbook on your own servers and get to work faster than ever.

]]>
https://www.ssdnodes.com/blog/secure-ansible-playbook/feed/ 0
Ansible security playbook for your VPS (part 2) https://www.ssdnodes.com/blog/secure-ansible-playbook-2/ https://www.ssdnodes.com/blog/secure-ansible-playbook-2/#respond Fri, 07 Sep 2018 00:00:00 +0000 http://ssdnodes.billabailey.com/2017/09/07/tutorial-a-more-secure-ansible-playbook-part-2/ https://www.ssdnodes.com/blog/secure-ansible-playbook-2/feed/ 0