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:

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:

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




