{"id":5918,"date":"2021-05-13T18:27:19","date_gmt":"2021-05-13T18:27:19","guid":{"rendered":"https:\/\/blog.ssdnodes.com\/blog\/?p=5918"},"modified":"2025-05-18T12:43:28","modified_gmt":"2025-05-18T12:43:28","slug":"ansible-initial-setup","status":"publish","type":"post","link":"https:\/\/www.ssdnodes.com\/blog\/ansible-initial-setup\/","title":{"rendered":"Using Ansible Playbooks to setup your server"},"content":{"rendered":"<p>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.<\/p>\n<\/p>\n<h2>Prerequisites<\/h2>\n<ol>\n<li>A <strong>Control Node<\/strong> 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.<\/li>\n<li>One or more <strong>Target or Hosts<\/strong>. We will be using another VM running Ubuntu 20.04 LTS as our Host, which, Ansible will configure for us.<\/li>\n<li>A <a href=\"https:\/\/www.ssdnodes.com\/blog\/vps-beginners-guide\/\" title=\"basic understanding of SSH and how to connect to a remote VPS\">basic understanding of SSH and how to connect to a remote VPS<\/a> and use it.<\/li>\n<\/ol>\n<h2>Goals<\/h2>\n<p>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:<\/p>\n<ol>\n<li>Add a public SSH Key for the <code>root<\/code> user, allowing us to login as the root user using our public-private SSH Key pair. Here's an introduction to <a href=\"https:\/\/www.ssdnodes.com\/blog\/connecting-vps-ssh-security\" title=\"SSH and SSH keys\">SSH and SSH keys<\/a><\/li>\n<li>Disable password-based authentication and allow only key-based logins which are much secure.<\/li>\n<li>Update all the packages on the system. Equivalent to running <code>apt update; apt upgrade<\/code> on Ubuntu or <code>dnf update<\/code> on CentOS and Fedora.<\/li>\n<\/ol>\n<p>So let's get started.<\/p>\n<h2>Ansible Installation and Basics<\/h2>\n<p>On your Control Node, Ansible can be installed using your system's package manager or Python's Package manager <code>pip<\/code>, since Ansible is written in Python. On macOS, it is recommended that you install it using <code>pip<\/code> or <code>pip3<\/code>:<\/p>\n<pre><code>$ pip install -U ansible<\/code><\/pre>\n<p>On Linux, you can get it straight from your system's package manager:<\/p>\n<pre><code>$ apt install ansible # For Debian or Ubuntu based systems\n$ dnf install ansible # For RedHat, Fedora or CentOS based systems<\/code><\/pre>\n<p>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 <strong>agentless automation engine<\/strong>. Because you don't have to install any specific software on the target.<\/p>\n<p>So now we also know how Ansible works. <\/p>\n<ol>\n<li>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. <\/li>\n<li>It uses Python3 (Python2 works but is deprecated, and not recommended) to run all the automation, checks and data collection on the hosts.<\/li>\n<\/ol>\n<h2>Configuring Ansible<\/h2>\n<p>There are three key files needed on the control node:<\/p>\n<ol>\n<li>Ansible <strong>Playbook(s)<\/strong> describing what automation to run on your hosts. This will be our main focus.<\/li>\n<li>An <strong>inventory<\/strong> file listing all your hosts and grouping them together in logical ways. On most Linux distros this file is <code>\/etc\/ansible\/hosts<\/code><\/li>\n<li><strong>Ansible's configuration file<\/strong>. On most Linux distros this file is <code>\/etc\/ansible\/ansible.cfg<\/code><\/li>\n<\/ol>\n<p>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:<\/p>\n<pre><code>$ mkdir playbooks\n$ touch ansible.cfg inventory<\/code><\/pre>\n<p>Ansible will automatically pick the current directory's <code>ansible.cfg<\/code> file and override the main configuration with this one. Edit the <code>ansible.cfg<\/code> file and add the following contents to it:<\/p>\n<pre><code>[defaults]\ninventory = .\/inventory<\/code><\/pre>\n<p>This will set the current directory's <code>inventory<\/code> 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. <strong>Make sure to use your actual IP address and not what is shown below<\/strong>:<\/p>\n<pre><code>127.0.0.1<\/code><\/pre>\n<p>The offical documentaion shows how you can create more complicated inventory capable of organizing <a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/user_guide\/intro_inventory.html\" title=\"hundreds of servers into dozens of categories\" target=\"_blank\" rel=\"noopener\">hundreds of servers into dozens of categories<\/a>.<\/p>\n<p>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.<\/p>\n<h2>Writing the playbook<\/h2>\n<p>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.<\/p>\n<p>Create a file called <code>initial-setup.yaml<\/code> and we can start building our playbook. The following first few lines <\/p>\n<pre><code>---\n- name: Initial Setup\n  hosts: all\n  remote_user: root\n<\/code><\/pre>\n<p>The beginning <code>---<\/code> describes the start of a YAML file, and is optional. Next, we create a list with each element of the list starting with <code>-<\/code>. There are going to be lists within lists in our yaml file and this is the outermost list, with just one item in it.<\/p>\n<p>The first element of this outermost list contains an Object (objects are like dictionaries in Python3) and this object has the following attributes:<\/p>\n<ul>\n<li><code>name: Initial Setup<\/code> which sets the name of our playbook as <em>Initial Setup<\/em><\/li>\n<li><code>hosts: all<\/code> selects which hosts from our inventory file will this playbook act upon. We have decided to act on all the hosts inside the inventory.<\/li>\n<li><code>remote_user: root<\/code> sets what user do we wish to SSH as into the remote host.<\/li>\n<\/ul>\n<p>The next item in this Object will be <code>tasks<\/code> and herein will lie the bulk of our &quot;configurations&quot;. I will show the tasks below as part of the larger playbook, because it is important to note that the indentation level on <code>tasks<\/code> should be same as <code>name<\/code>. However, <code>tasks<\/code> itself has a list of, well, <em>tasks<\/em> within it. Each <em>task<\/em> is another object. an indentation level below <code>tasks<\/code> and <code>name<\/code>. And each task comes with a name and a module along with the action that the module is supposed to take.<\/p>\n<pre><code>---\n- name: Initial Setup\n  hosts: all\n  remote_user: root\n  tasks:\n  - name: Add SSH key for root\n    lineinfile:\n      path: ~\/.ssh\/authorized_keys\n      create: yes\n      state: present\n      line: &lt;COPY YOUR PUBLIC SSH KEY HERE&gt;<\/code><\/pre>\n<p>So the first <em>task<\/em> here is named &quot;Add SSH key for root&quot; and it uses an Ansible module <code>lineinfile<\/code> which makes sure that a specific line is present (or absent) in a given file. Here, the <code>lineinfile<\/code> module also contains a dictionary of following parameters within it.<\/p>\n<ul>\n<li><code>path: ~\/.ssh\/authorized_keys<\/code> specifies which file this particular task is concerned with.<\/li>\n<li><code>create: yes<\/code> says that, if the file is absent, create it!<\/li>\n<li><code>state: present<\/code> specifics that we want a given line to be present.<\/li>\n<li><code>line: &lt;COPY YOUR SPECIFIC SSH KEY HERE&gt;<\/code> specifics what line we want there to be.<\/li>\n<\/ul>\n<p>Different modules will have different parameters. For example, <code>package<\/code> module will not have a <code>path<\/code> or <code>line<\/code> variables because the installation of a package has nothing to do with path or lines.<\/p>\n<p>Obviously, no one can remember how each module works and what parameters it takes. So, when you are writing your playbooks, <a href=\"https:\/\/docs.ansible.com\/ansible\/2.9\/modules\/list_of_all_modules.html\" title=\"the ansible documentation\" target=\"_blank\" rel=\"noopener\">the ansible documentation<\/a> will be your best guide. For example, the <code>lineinfile<\/code> module has all its various parameters described, along with the default values, and examples, <a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/collections\/ansible\/builtin\/lineinfile_module.html\" title=\"here\" target=\"_blank\" rel=\"noopener\">here<\/a>. The documentation is clean, easy to follow, and full of relevant examples.<\/p>\n<p>You can see in the docs that <code>state: present<\/code> 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.<\/p>\n<p>Ansible's automation system has hundreds of built-in modules, and you can also access many more community provided modules from <a href=\"https:\/\/galaxy.ansible.com\/\" title=\"ansible-galaxy\" target=\"_blank\" rel=\"noopener\">ansible-galaxy<\/a>. <\/p>\n<h2>Running the playbook<\/h2>\n<p>To run the above playbook, switch to the directory where the playbook lives and use the <code>ansible-playbook<\/code> command:<\/p>\n<pre><code>$ cd playbooks\/\n$ ansible-playbook initial-setup.yaml<\/code><\/pre>\n<p>If you have not set your SSH keys, and are going to login using password, install <code>sshpass<\/code> on your local machine and use <code>ansible-playbook<\/code> with the flag <code>--ask-pass<\/code> to allow ansible to login using plain text password:<\/p>\n<pre><code>$ sudo apt install sshpass\n$ ansible-playbook --ask-pass initial-setup.yaml<\/code><\/pre>\n<p>Enter the password for your VPS when prompted.<\/p>\n<p>The above command will be required only for the first time, since we are adding SSH keys as part of our server configuration.<\/p>\n<h2>Idempotent<\/h2>\n<p>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.<\/p>\n<p>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 <code>authorized_keys<\/code> file. But if you just do a <code>cat keyfile.pub &gt;&gt; authorized_keys<\/code> 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.<\/p>\n<p>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.<\/p>\n<h2>Adding more stuff to our playbook and using Conditionals<\/h2>\n<p>Let's get the ball rolling with a slightly more complicated <em>task<\/em>. To upgrade all the system packages.<\/p>\n<p>For Debian\/Ubuntu hosts we will use the <code>apt<\/code> module.<\/p>\n<pre><code>  - name: Update all packages for Debian-like Systems\n    apt:\n      update_cache: yes\n      name: &#039;*&#039;\n      force_apt_get: yes\n      state: latest<\/code><\/pre>\n<p>Consult the documentation for the <a href=\"https:\/\/docs.ansible.com\/ansible\/latest\/collections\/ansible\/builtin\/apt_module.html\" target=\"_blank\" rel=\"noopener\"><code>apt<\/code> module<\/a> to understand various parameters here.<br \/>\nObviously, the same module won't work on CentOS, RedHat and Fedora like systems, so we need to ensure that this works only for the <code>Debian<\/code> 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.<\/p>\n<p>We can use this information to create a condition statement like so:<\/p>\n<pre><code>  - name: Update all packages for Debian-like Systems\n    apt:\n      update_cache: yes\n      name: &#039;*&#039;\n      force_apt_get: yes\n      state: latest\n    when: ansible_facts[&#039;os_family&#039;] == &#039;Debian&#039;<\/code><\/pre>\n<p>The <code>when<\/code> needs to be at the same indentation level as <code>apt<\/code> and <code>name<\/code>. 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.<\/p>\n<pre><code>  - name: Update all packages for RHEL like systems\n    dnf:\n      update_cache: yes\n      name: &quot;*&quot;\n      state: latest\n    when: ansible_facts[&#039;os_family&#039;] == &#039;RedHat&#039;<\/code><\/pre>\n<h2>Looping through a list of items in a task<\/h2>\n<p>The above two tasks of updating and upgrading the systems are the only ones that are OS specific. Ansible has a generic module called <code>package<\/code> 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:<\/p>\n<pre><code>  - name: Install Packages\n    package:\n      name: &quot;{{packages}}&quot;\n      state: present\n    vars:\n      packages:\n        - vim\n        - curl\n        - wget\n        - nginx<\/code><\/pre>\n<p>Notice, we did something more intricate here. Instead of rewriting a list of tasks, all of which uses <code>package<\/code> module, like below:<\/p>\n<pre><code>  - name: Install vim\n    package:\n      name: vim\n      state: present<\/code><\/pre>\n<p>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 <code>vars<\/code> keyword is special to Ansible and is used to declare variables, in this case the variable is called <code>packages<\/code> which itself has a list of package names inside it. Ansible sees the variable name inside curly braces like this <code>{{packages}}<\/code> and understands to loop over the list of items.<\/p>\n<p>There are other ways of looping as well using keywords <code>loop<\/code> and <code>with_items<\/code>.<\/p>\n<h2>Editing Configuration file<\/h2>\n<p>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 <code>lineinfile<\/code> module to rewrite our <code>\/etc\/ssh\/sshd_config<\/code> file and this time we will add a <code>handler<\/code> (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.<\/p>\n<pre><code> - name: Configure SSH Daemon\n    lineinfile:\n      path: \/etc\/ssh\/sshd_config\n      regexp: &#039;^[(#)|(# )]?PasswordAuthentication [(yes)|(no)]+$&#039;\n      line: &#039;PasswordAuthentication no&#039;\n   notify: Restart SSHD\n\n  handlers:\n    - name: Restart SSHD\n      service:\n        name: sshd\n        state: restarted<\/code><\/pre>\n<p>This needs a bit of explanation. The <code>lineinfile<\/code> module, here, searches for a given <code>regexp<\/code> (a regular expression) which is essentially a pattern. Here, the pattern says look for a line that may start with <code>#<\/code> or the same symbol followed with a whitespace, or nothing, followed by <code>PasswordAuthentication<\/code>, followed by a space and then either a <code>yes<\/code> or a <code>no<\/code> followed by end of line, described by <code>+$<\/code>. This may include the following lines:<\/p>\n<ul>\n<li><code>#PasswordAuthentication yes<\/code><\/li>\n<li><code># PasswordAuthentication yes<\/code><\/li>\n<li><code>PasswordAuthentication yes<\/code><\/li>\n<\/ul>\n<p>And the same three patterns are repeated with <code>no<\/code> at the end.<\/p>\n<p>Once such a pattern is found, the <code>lineinfile<\/code> module replaces it with <code>PasswordAuthentication no<\/code> and then, if the state of the file changes, it notifies the handler <code>Restart SSHD<\/code> 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.<\/p>\n<p>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 <a href=\"https:\/\/youtu.be\/bgBWp9EIlMM\" title=\"really great video on the topic\" target=\"_blank\" rel=\"noopener\">really great video on the topic<\/a>.<\/p>\n<h2>Conclusion<\/h2>\n<p>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.<\/p>\n<pre><code>---\n- name: Initial Setup\n  hosts: all\n  remote_user: root\n\n  tasks:\n  - name: Add SSH key for root\n    lineinfile:\n      path: ~\/.ssh\/authorized_keys\n      create: yes\n      state: present\n      line: &lt;YOUR PUBLIC SSH KEY HERE&gt;\n\n  - name: Configure SSH Daemon\n    lineinfile:\n      path: \/etc\/ssh\/sshd_config\n      regexp: &quot;{{ item.regexp }}&quot;\n      line: &quot;{{ item.line }}&quot;\n    with_items:\n       - { regexp: &#039;^[(#)|(# )]?Port[ 0-9]+$&#039;, line: &#039;Port 22&#039; }\n       - { regexp: &#039;^[(#)|(# )]?PermitRootLogin [(yes)|(no)|(without\\-password)]+$&#039; , line: &#039;PermitRootLogin without-password&#039; }\n       - { regexp: &#039;^[(#)|(# )]?PasswordAuthentication [(yes)|(no)]+$&#039;, line: &#039;PasswordAuthentication no&#039; }\n    notify: Restart SSHD\n\n  - name: Update all packages for Debian-like Systems\n    apt:\n      update_cache: yes\n      name: &#039;*&#039;\n      force_apt_get: yes\n      state: latest\n    when: ansible_facts[&#039;os_family&#039;] == &#039;Debian&#039;\n    notify: Autoremove Packages using APT\n\n  - name: Update all packages for RHEL like systems\n    dnf:\n      update_cache: yes\n      name: &quot;*&quot;\n      state: latest\n    when: ansible_facts[&#039;os_family&#039;] == &#039;RedHat&#039;\n    notify: Autoremove Packages using DNF\n\n  - name: Install Packages\n    package:\n      name: &quot;{{packages}}&quot;\n      state: present\n    vars:\n      packages:\n        - vim\n        - curl\n        - wget\n\n  handlers:\n    - name: Autoremove Packages using DNF\n      dnf:\n        autoremove: yes\n\n    - name: Autoremove Packages using APT\n      apt:\n        autoremove: yes\n\n    - name: Restart SSHD\n      service:\n        name: sshd\n        state: restarted<\/code><\/pre>\n<p>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!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>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  &#8230;<\/p>\n","protected":false},"author":20,"featured_media":6072,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[18,30],"tags":[209],"class_list":["post-5918","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","category-tutorials","tag-ansible"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/5918","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/users\/20"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/comments?post=5918"}],"version-history":[{"count":3,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/5918\/revisions"}],"predecessor-version":[{"id":13000,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/5918\/revisions\/13000"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/media\/6072"}],"wp:attachment":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/media?parent=5918"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/categories?post=5918"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/tags?post=5918"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}