Lab: Installing the MEAN Stack with Ansible

Introduction

This lab introduces you to Ansible and how it can be used to automate the MEAN stack installation. When you finish, you will be able to:

  • Create simple tasks with Ansible
  • Execute the tasks in your remote nodes in order to deploy the stack

You can see a Nugget for this lab done here: Install MEAN with Ansible.
Lab servers

NOTE: If you don’t have experience with Linux Academy lab servers, complete the Introduction to Linux Academy course before you continue this lab. That course explains how to create a server and work with it.

Lab requirements:

  • Ubuntu 14
  • Ansible 2.2.0

Use Linux Academy lab servers to create the following Ubuntu 14 instances:

  • Master Instance (master.mylabserver.com): Executes the Ansible tasks.

  • Node instance (node.mylabserver.com): Provides the location where the deploy will be done automatically from our master instance.

TIP: Attached configuration files can be different depending on previous configuration or software versions.

Set up our master instance

We’ll need to log in on our master instance before we can install and configure Ansible.

Install Ansible

Ansible requires Python to work, so our Ubuntu master instance needs a package called software-properties-common which includes Python libraries:

$ sudo apt-get install software-properties-common

Next add the Ansible repository:

$ sudo apt-add-repository ppa:ansible/ansible

And then you are ready to update the packages list and install Ansible:

$ sudo apt-get update
$ sudo apt-get install ansible
Configure Ansible

Ansible has a large file with many configuration parameters located on /etc/ansible/ansible.cfg by default. In this particular server lab, we only need to specify where our inventory is located and who our sudo user is. The inventory is a file where we define our server infrastructure.

Open the /etc/ansible/ansible.cfg file using your preferred editor, and then uncomment the inventory parameter line and the sudo_user parameter line. Your Ansible configuration file should look like this:

# config file for ansible -- http://ansible.com/
# ==============================================
# nearly all parameters can be overridden in ansible-playbook
# or with command line flags. ansible will read ANSIBLE_CONFIG,
# ansible.cfg in the current working directory, .ansible.cfg in
# the home directory or /etc/ansible/ansible.cfg, whichever it
# finds first
[defaults]
# some basic default values...
inventory = /etc/ansible/hosts
#library = /usr/share/my_modules/
#remote_tmp = $HOME/.ansible/tmp
#local_tmp = $HOME/.ansible/tmp
#forks = 5
#poll_interval = 15
sudo_user = root
#ask_sudo_pass = True
#ask_pass = True
#transport = smart
#remote_port = 22
#module_lang = C
#module_set_locale = False
[...] more Ansible parameters[...]

Create the Ansible inventory

Now Ansible needs to know the names of our nodes to execute tasks on them. To do this, Ansible has a file called hosts inside the /etc/ansible directory. Open this file using your preferred editor to see examples of how hosts can be defined. Make a backup of this file (in case that you want to keep it) and overwrite its content with this:

[local]
localhost
[nodes]
node.mylabserver.com

When Ansible tasks are executed, we define which hosts we want the tasks executed on. In this case, we created two groups of servers: local and nodes. The nodes group will include all node instances. This configuration is very flexible, so you can create your own.

For example, if you have four machines (two Ubuntus and two CentOS) you can create a hosts file like this:

[ubuntu]
ubuntu1.mylabserver.com
ubuntu2.mylabserver.com
[centos]
centos1.mylabserver.com
centos2.mylabserver.com

Or if you have three instances, one for development, one for production and another one for the database, your hosts file would look like:

[development]
dev.mylabserver.com
[production]
prod.mylabserver.com
[database]
db.mylabserver.com

Using this type of notation you can create a very readable hosts file which fits your infrastructure.

Create Ansible users

It is recommended that you create a user for Ansible so your system will stay organized. To create a user for Ansible, we use the adduser command:

$ adduser ansible

NOTE: Ubuntu will ask for a password and information such as name, organization, etc., for this user. Type a secure password and remember it because we will use it later.

Because our Ansible tasks will use some privileged commands such as apt-get, we are going to edit the sudoers file so the system won’t prompt us for the password every time we execute a sudo command. To do this, we need to execute the visudo command as root:

$ sudo visudo

Next, we will add a new line below our root user privilege specification. Our file will look like this:

#
# This file MUST be edited with the 'visudo' command as root.
#
# Please consider adding local content in /etc/sudoers.d/ instead of
# directly modifying this file.
#
# See the man page for details on how to write a sudoers file.
#
Defaults env_reset
Defaults mail_badpass
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin"
# Host alias specification
# User alias specification
# Cmnd alias specification
# User privilege specification
root ALL=(ALL:ALL) ALL
ansible ALL=(ALL) NOPASSWD: ALL
# Members of the admin group may gain root privileges
%admin ALL=(ALL) ALL
# Allow members of group sudo to execute any command
%sudo ALL=(ALL:ALL) ALL
# See sudoers(5) for more information on "#include" directives:
#includedir /etc/sudoers.d

As you can see, we added a new line setting the NOPASSWD parameter for our new user. Now the user can execute any sudo command without being prompted for the password.

Set up our node instance

Our node instance doesn’t need to have Ansible installed. We do need the user for Ansible so we can communicate with our node from the master instance. To create the user, we repeat the process of creating a user exactly as we did before on our master instance (adduser and visudo) but on each node instance; see Create Ansible users.

Create communication between the master and node

The master instance will execute Ansible tasks on our node instance, so we need to create a way for our master instance to communicate with the node instance. We can use ssh to do this.

We are going to create an ssh public key on our master instance first; be sure that you are logged in as your ansible user on your master instance. You can switch to your ansible user using the su command:

$ su - ansible

Now that you are using your master instance as the ansible user, create the public key using ssh-keygen command:

$ ssh-keygen

The system will ask for a passphrase. For this server lab we will leave the passphrase empty, so press the return key for every question until the process completes.

Now we need to export this public key to our node instance. To accomplish that, we use the ssh-copy-id command to specify the node instance:

$ ssh-copy-id ansible@node.mylabserver.com

We export the key using the ansible user to connect to our node instance. After that you will see a success message.

Due to the nature of the lab servers and how they change IP addresses every time they start, maybe you will receive a ERROR: Host key verification failed when doing the ssh-copy-id command. To fix this, we have to turn off strict host key checking. You can do this by editing/creating a config file in the /home/ansible/.ssh directory and adding these lines:

Host *
StrictHostKeyChecking no

This needs to be on the server we are trying to ssh from (in this case, our master instance).

Create an Ansible playbook of tasks

Now that both master and node instances are set, we are ready to create our Ansible tasks to install the MEAN stack.

To create a playbook of tasks, we need to divide our problem into tasks. In our guide How to Install MEAN on Ubuntu we explained that MEAN stack needs a few technologies to work properly (Git, MongoDB and Node.js).

In order to create a good Ansible playbook, we are going to create four files: a main file called main.yml, and three more files (prerequisites.yml, mongodb.yml and nodejs.yml) for each big task that we are going to accomplish (install Git, install MongoDB and install Node.js). These three last files are going to be inside a folder called tasks to keep everything in order. As the Ansible user we are going to create these files inside our home directory, and the main.yml file will look like this:

---
- hosts: nodes
remote_user: ansible
become: yes
become_method: sudo
vars:
temp_folder: /tmp
tasks:
# Install prerequisites
- include: tasks/prerequisites.yml
# Install MongoDB
- include: tasks/mongodb.yml
# Install Node.js
- include: tasks/nodejs.yml

In this main.yml file, we told Ansible that our playbook will run on the node group (if you remember, our node group includes the node.labserver.com instance where we are going to deploy the MEAN stack), and it will use the ansible user (that we setup previously on both master and node instances).

We have to tell Ansible that we are going to execute some privilege commands. Ansible has a system to allow a user to execute tasks as another. In this case we only need to set that we are going to execute privilege tasks using the sudo method; this is because we have already set that the ansible user won’t need to type a password when executing privilege commands.

We also defined a variable called temp_folder that we are going to use when installing Node.js. You can define as many variables as you want, and it is always a good idea to abstract as much as we can from our tasks. In this case, a temporary folder path as a variable is enough.

In the section called tasks, we set all tasks that we want to be executed on node group. In this case, we have divided our playbook in three files, so here we include these files. Let’s see what specific tasks do we need, and how can we code them in Ansible.

Prerequisites for MEAN

To install the MEAN stack, we need to install Git first. On Ubuntu, we use the command apt-get to install packages. In this case, Ansible has a module called apt which does exactly the same as apt-get, but with a different syntax. You will see that Ansible has modules for almost everything, so you don’t need to hard code a command in most cases.

A task for installing Git using Ansible may look like this:

- name: Install git  
apt:
name: git
state: present
update_cache: yes

We used the name parameter to specify the package name (git in this case). Next, we tell the apt module to install the package version available in the repositories using the state present value.

In this case we established that the state of the package will be present, which means that Ansible will try to install it only if it doesn’t exist. This option can also be latest in case that we want the package to be updated even if it had already installed on the system.

We also want to update the repository list before installing this package, so we run the equivalent of apt-get update by setting update_cache parameter to yes. This update will be done before the installation. By using this structure, everything is enclosed in the same task. However, you can always initiate this update in a separate task. This allows us to define our Git installation in two separate tasks:

- name: Update repositories  
apt:
update_cache: yes
- name: Install git
apt:
name: git
state: present

As you can see, both tasks have descriptive names. Since we will often define dozens of tasks, it is important to use clear names for each one. Names like “task 1” won’t provide any information at all if something goes wrong, so it is a good practice to keep a good nomenclature.

MongoDB tasks

Now we are going to install MongoDB. Looking forward, we will need to accomplish the following: 

  1. Import the MongoDB public key.
  2. Add MongoDB repository.
  3. Install MongoDB
  4. Check if the service is running.

Each item could be a task by itself, so let’s create each task individually and compile them into a single file.

Import MongoDB public key

There is an Ansible module called apt_key to manage the addition and removal of public repository keys. If you remember, the command to add this key is:

$ sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv EA312927

Using the apt_key module, our task will look like:

- name: MongoDB | Import public key
apt_key:
keyserver: hkp://keyserver.ubuntu.com:80
id: EA312927

This gives us a very easy way to define which server we want to access and allows us to provide the ID of the public key we want to import. Because we are adding a new key, we don’t need to specify a state for this task. On the other hand, if we wanted to remove this key, we can set its state to absent and the key will be removed.

Add MongoDB repository

To add a new repository, we can use the apt_repository package. In our MEAN installation guide, we saw that we can add a repository using this command:

$ echo "deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.2 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.2.list

We used pipe to echo the result of a deb command inside a file. We can create the same effect using the apt_repository module:

- name: MongoDB | Add repository
apt_repository:
filename: '/etc/apt/sources.list.d/mongodb-org-3.2.list'
repo: 'deb http://repo.mongodb.org/apt/ubuntu trusty/mongodb-org/3.2 multiverse'
state: present
update_cache: yes

Using apt_repository allows us to give a name to the location we are going to dump the repository content. We also set the state as present because we want to add the repository, and we set the update_cache parameter to yes to force a repository update after the addition.

Install MongoDB

Now that we have added the MongoDB public key and its repository, we are ready to install it. As you may have guessed, we are going to use the apt module again:

- name: MongoDB | Install MongoDB
apt:
name: mongodb-org
state: present
update_cache: yes
Check MongoDB service

On Ubuntu, after the installation of MongoDB, the service will start automatically. However, we need to ensure that it is actually running as expected.

There are multiple ways to do this task, but one of the simplest solutions is to use the service module. Using this module we can start, stop, and reload any service on our system. A simple task to start our MongoDB service can be done like this:

- name: MongoDB | Ensure that MongoDB is running
service:
name: mongod
state: started
Node.js tasks

The Node.js installation is quick using direct commands, but doing it with Ansible requires a few extra tasks.

Essentially, a Node.js installation is done by downloading and executing a script provided by Node.js itself, then updating the repository and executing the installation with apt. On Ubuntu, the script execution can be done using a simple command:

$ curl -sL http://deb.nodesource.com/setup_6.x | sudo -E bash -

The curl command downloads the script from a remote location, and we execute it directly by piping (I feel like this sounds better, but it may be improper?) it to bash.

In Ansible, we can use the command package to execute commands directly, but we should always try to use Ansible packages where possible. In addition to clearly defining each task, Ansible packages and modules are designed to be idempotent and provide good error handling. With that in mind, we need to download the script and execute it directly on our node instance.

Ansible has several modules which will help us. For instance, get_url provides a way to download files:

- name: Node.js | Get script
get_url:
url: "http://deb.nodesource.com/setup_6.x"
dest: "{{ temp_folder }}/nodejs.sh"

Recall when we defined a variable in our main.yml file with the temporary folder path earlier in the guide. We are using it here. To use a variable, we enclose it in double brackets.

Now that the Node.js script is downloaded in our temporary folder, we need to execute it. On Unix system files need to have execution rights to allow us its execution. We can grant this permission using the file module:

- name: Node.js | Set execution right to script
file:
path: "{{ temp_folder }}/nodejs.sh”
mode: "u+x"

With this task we are adding the execution right to Node.js installation script. The file module allow us to specify grants using octal mode too.

Now that the Node.js script is downloaded in our temporary folder and have execution rights, let’s execute it using the shell module:

- name: Node.js | Execute installation script
shell: "{{ temp_folder }}/nodejs.sh"

After the execution of the script, it won’t be needed again. A good practice would be to remove this file using file module:

- name: Node.js | Remove installation script
file:
path: "{{ temp_folder }}/nodejs.sh"
state: absent

Now that the Node.js repository is added to our system, we can start with the installation. Node.js requires the build-essential package. We could divide this task in two subtasks: install build-essential, then install nodejs, but there is a good way to accomplish the same in a single task using a parameter called with_items.

The with_items parameter works as a for loop: a task will be executed for each element inside the with_items parameter. Let’s see how our Node.js installation task will look:

- name: Node.js | Install Node.js
apt: name={{ item }} state=present update_cache=yes
with_items:
- build-essential
- nodejs

Using the apt module again, we have defined that the package name will be read from a variable. Combined with the with_items parameter, it will execute the command for each package listed. This is a very compact way to execute the same task for multiple variables (in this case, the package name).

Node.js is now installed, and we can move on to install a few npm packages necessary to start developing our MEAN app.

Install bower and gulp

To install bower and gulp we can use the npm module. Both packages need to be installed globally. Again, we can perform both installations in a single task using the with_items parameter:

- name: Node.js | Install bower and gulp globally
npm: name={{ item }} state=present global=yes
with_items:
- bower
- gulp
Executing our playbook

Now that we defined all of our tasks, we are ready to execute them. As a reminder, we have four files on our master instance:

  • main.yml: our playbook which includes the following files:
  • tasks/prerequisites.yml: tasks to install prerequisites of the MEAN stack (in this case, install Git).
  • tasks/mongodb.yml: tasks to install MongoDB.
  • tasks/nodejs.yml: tasks to install Node.js.

It’s time to execute the main.yml file, so make sure you are logged into your master instance as your ansible user. If not, you can change users with this command:

$ su ansible -

Then, to execute our Ansible playbook, we run the following command:

$ ansible-playbook main.yml

This will execute our playbook on all nodes belonging to the node group since we set it up to do so. Recall the main.yml file:

---
- hosts: node
remote_user: ansible
become: yes
become_method: sudo
vars:
temp_folder: /tmp
tasks:
# Install prerequisites
- include: tasks/prerequisites.yml
# Install MongoDB
- include: tasks/mongodb.yml
# Install Node.js
- include: tasks/nodejs.yml

To ensure everything will execute properly, it’s a good idea to use the Ansible-playbook command parameter called check:

$ ansible-playbook main.yml --check

This will list all changes that are going to be made in our nodes without actually executing them. It is good practice to execute our playbook like this to ensure the results match our expectation. If something is incorrect, we can investigate and fix the problems before applying the changes.

Conclusion

In this server lab, we learned how to use Ansible to automate the installation of the MEAN stack. As you might notice, we didn’t set up almost anything (perhaps I’m misunderstanding the intention, but I think this line can be removed). Now we should configure our MongoDB, deploy our project, configure it, etc.

We can do all of this using Ansible modules in the same way that we did on this server lab. You should be able now to create simple tasks which fit your own needs (for example, clone a repository using the git module, overwrite a configuration file using the copy module, etc).

Related links

  • post-author-pic
    Davis E
    01-06-2017

    Excellent work! This is a good guide. 

  • post-author-pic
    Francisco S
    01-06-2017

    Thanks to  @davisengeler

      for making an incredible Nugget for this lab. Check it out! https://linuxacademy.com/cp/nuggets/view/id/134 

  • post-author-pic
    Derek M
    01-06-2017

    Awesome job! I can't wait to work with this! 

  • post-author-pic
    Johnny J
    01-06-2017

     @franverona: This is nice! Great job!

  • post-author-pic
    Francisco S
    01-24-2017

    Thanks guys! :D

  • post-author-pic
    Olaleye A
    05-09-2017

    Thumbs up Francisco. Well laid out.  Thanks

  • post-author-pic
    Robert C
    05-12-2017

    All worked without a hitch. Appreciate the good work.

  • post-author-pic
    Andrey S
    08-02-2017

    Great lab. Thanks a lot.

    PS:

    with --check option we cannot check

    TASK [MongoDB | Install MongoDB]

    because the package is not installed

    and we get error like this:

    No package matching 'mongodb-org' is available


Looking For Team Training?

Learn More