Deploying a Web App to a Server with Ansible, Part I: API

Photo by WrongTog on Unsplash

Deploying a Web App to a Server with Ansible, Part I: API

During the past few years, I have been developing a math-related web app. Now I wanted to deploy it to my LAMP server that I have discussed in some previous articles. I've decided to use Ansible, and I've described setting it up in WSL in this article.

Preparing the Server

For first, I had to do some preparations on the server. I SSHed to the server and ran the following commands:

sudo apt-get update
sudo apt-get install nodejs
sudo apt-get install npm

Next, I tried to install the PM2 Node process manager globally via NPM. This failed as I wasn't executing the install command with sudo. However, using sudo to install NPM packages is a security risk, so I had to use a different approach. I wanted to keep the global packages in my home directory (/home/username), so I first executed this command:

npm config set prefix ~/.local

Then I created a .bashrc file in my home directory like this...

nano .bashrc

...and added the following to it:

export PATH=/home/maija/.local/bin/:$PATH

I then ran this command to source the .bashrc (I was in sh, not the bash shell, so the source command was not working):

. ./.bashrc

Now, I could finally install PM2 with this command:

npm install -g pm2

...and the install was successful.

The API Ansible Playbook Line by Line

My web app consists of a headless Node.js API app, an administration interface written with Node as well, and a static React frontend. Here's a diagram of the app:

220314-math-diagram.png

I had previously installed MySQL and created a database in it. Now, I wanted to deploy the headless API. I added a directory named ansible to my app folder and created a playbook file named api.yaml. Next, I will go through the file line by line.

The first lines just define the name of the playbook and the targeted hosts. I have just one host in my local /etc/ansible/hosts file (in WSL), so that will be targeted.

---
- hosts: all
  name: Math API

Next, I define the tasks included in the playbook. The first tasks create some directories. They set the file permissions to 755, i.e., readable and executable by anyone but writable only by the owning user. The directories will include the source code from GitHub, the runnable app files, and log files, respectively:

  tasks:
  - name: Create ts-math directory
    ansible.builtin.file:
      path: /home/maija/ts-math
      state: directory
      mode: '0755'

  - name: Create app directory
    ansible.builtin.file:
      path: /home/maija/apps/math-api
      state: directory
      mode: '0755'

  - name: Create log directory
    ansible.builtin.file:
      path: /home/maija/log
      state: directory
      mode: '0755'

Next, I'll checkout all the code from GitHub. I added the force parameter to overwrite files that are edited automatically (like package-lock.json). The repository is public, so no hassle with keys here:

  - name: Checkout code from git repo
    ansible.builtin.git:
      repo: 'https://github.com/mkkekkonen/TS-Math.git'
      dest: /home/maija/ts-math
      force: yes

I then upload a file not in source control that includes secrets needed by the app. It will be readable and writable by the owning user, readable by everyone, and executable by no one:

  - name: Upload secrets file
    ansible.builtin.copy:
      src: /mnt/c/path/to/directory/ansible/secrets.json
      dest: /home/maija/ts-math/api/src/assets/json/secrets.json
      owner: maija
      group: maija
      mode: '0644'

I then perform some npm-related tasks. The following tasks will install the API packages, delete the previous build directory if it exists, and build the project.

  - name: Install packages
    ansible.builtin.shell:
      chdir: /home/maija/ts-math/api
      cmd: npm install > /home/maija/log/api-install-log.txt

  - name: Delete dist directory
    ansible.builtin.file:
      path: /home/maija/ts-math/api/dist
      state: absent

  - name: Build project
    ansible.builtin.shell:
      chdir: /home/maija/ts-math/api
      cmd: npm run build > /home/maija/log/api-build-log.txt

Next, I transfer the needed files to the app folder. I first copy the contents of the api/dist directory (note the slash after the directory name) to the apps/math-api folder. Then, I copy the package.json file there. Last, I perform an ugly hack by symlinking the node_modules directory (ts-math -> apps) so that I don't need to install the packages again. The app is built with the TypeScript tsc command so the output does not include the code in node_modules.

  - name: Copy built files
    ansible.builtin.copy:
      src: /home/maija/ts-math/api/dist/
      dest: /home/maija/apps/math-api
      remote_src: yes

  - name: Copy package.json
    ansible.builtin.copy:
      src: /home/maija/ts-math/api/package.json
      dest: /home/maija/apps/math-api/package.json
      remote_src: yes

  - name: Link node_modules
    ansible.builtin.file:
      src: /home/maija/ts-math/api/node_modules
      dest: /home/maija/apps/math-api/node_modules
      owner: maija
      group: maija
      state: link

Only the final PM2-related tasks are remaining. I upload the ecosystem file required by PM2 and restart the app. I specify /bin/bash as the shell executable so that the .bashrc file with the global npm packages will be correctly sourced.

  - name: Upload pm2 ecosystem file
    ansible.builtin.copy:
      src: /mnt/c/Users/mkkek/Documents/koodi/js/TS-Math/ansible/mysqlApi.ecosystem.config.js
      dest: /home/maija/apps/math-api/ecosystem.config.js

  - name: Restart app
    ansible.builtin.shell:
      executable: /bin/bash
      chdir: /home/maija/apps/math-api
      cmd: pm2 restart ecosystem.config.js

In the end, I was able to fetch data from the server using the URLs defined in source code:

220314-json.png

Sources:

Ansible Documentation: ansible.builtin.file
Ansible Documentation: ansible.builtin.git
Ansible Documentation: ansible.builtin.shell
michaelb: The Right Way™ to do global npm install without sudo
Namecheap: File permissions
PM2 - Home
Stack Overflow: Ansible: copy a directory content to another directory
Stack Overflow: Error message 'source: not found' when running a script