Deploying Node.js Apps with Ansible – closingtags </>
Categories
Automation Javascript Server

Deploying Node.js Apps with Ansible

‘Automating’ comes from the roots ‘auto-‘ meaning ‘self-‘, and ‘mating’, meaning ‘screwing’.

‘Automating’ comes from the roots ‘auto-‘ meaning ‘self-‘, and ‘mating’, meaning ‘screwing’.

While working on a side project, I discovered that serving the application as static assets from a sub-directory on my web server wasn’t going to work. Thanks to CORS, and the fact my application is built with SvelteKit, the only other option for me was to run a Node.js instance. I wanted to get it set up quickly so that I could prioritize application development, but I also wanted to take this opportunity to clean up my homelab automation repository. What if I need to run another Node.js instance later on? It seemed obvious the right thing to do was to spend a week and half automating something I could have accomplished in an afternoon.

Important note: I only spent a week and half on this because I was missing a semi-colon in a configuration file completely unrelated to all of this. 🙃

Background

I’ve previously written about how I have automated much of my homelab management with Ansible. If you’re unfamiliar with Ansible, and spend a significant amount of time managing multiple computers/servers, do yourself a favor and quit reading this blog post and go start learning Ansible. A great series of video tutorials can be found over at Learn Linux TV.

Unfortunately for me, I learned about Ansible after having already set up much of my homelab. Fortunately for you, I’ve been maintaining a repository since I discovered it. Now, as I need new functionality, I build it in to the repo instead of managing my homelab manually. Should my homelab (or home) ever go up in smoke, it will be significantly easier to set it all up again.

Automation

Enough of the snooze-fest, let’s automate!

Firstly, go open my homelab automation repository at https://github.com/Dilden/Ansible-Proxmox-Automation. Peruse that if you like then go ahead and give it a star while you’re at it.

Next, notice how I’ve incorporated roles. Roles make it simple to partition and reuse functionality. For instance, in the roles directory, I’ve created another directory; nodejs. This lets Ansible know to treat the directory name as a role which can now be used within playbooks. When I want to install Node.js on any new server, I can specify the target within my playbook, and simply call the nodejs role. This makes playbooks easier to read and write. It also simplifies the organization of logic.

Node.js

I’m not going to dig into how to create servers. If you’re interested in that, check out my playbook books/create-containers.yml and my proxmox role. For now, let’s focus on installing Node.js on an existing server. In this example, the server is referred to as nodejs in the inventory and is running Ubuntu 20.04 (Focal Fossa).

Playbook

---
- hosts: nodejs
  roles:
    - nodejs

This is it for the playbook. Seriously. This playbook will target nodejs (from the inventory file) and run the tasks from the nodejs role.

Role

---
- name: Install GPG
  tags: nodejs, install, setup
  apt:
    name: gnupg
    update_cache: yes
    state: present

- name: Install the gpg key for nodejs LTS
  apt_key:
    url: "https://deb.nodesource.com/gpgkey/nodesource.gpg.key"
    state: present

- name: Install the nodejs LTS repos
  apt_repository:
    repo: "deb https://deb.nodesource.com/node_{{ NODEJS_VERSION }}.x {{ ansible_distribution_release }} main"
    state: present
    update_cache: yes

- name: Install NodeJS
  tags: nodesjs, install
  apt:
    name: nodejs
    state: latest

One of the wonderful things about Ansible is that it remains easy to read. Just in case you struggled with it, here’s what the 4 tasks do:

  1. Install “gpg” package
  2. Add the repository key for NodeSource
  3. Install the specified version of Node for the specified distribution. These variables are located at defaults/main.yml within the role.
  4. Install the latest version of Node.js

defaults/main.yml

---
NODEJS_VERSION: "18"
ansible_distribution_release: "focal"

Now, installing Node.js on the server is as simple as running ansible-playbook INSERT_YOUR_PLAYBOOK_NAME_HERE.yml

Deploy the App 🚀

Getting Node.js running on your server is one thing but how do you get your code to that server? It’s a little more complicated.

Playbook

---
- hosts: nodejs
  roles:
    - app

Not this part. This part is simple.

Role

- name: Build app locally
  tags: app, build, deploy
  shell: npm run build
  args:
    chdir: ~/Dev/projects/app/
  delegate_to: 127.0.0.1

- name: Copy build to server
  tags: app, build, deploy
  copy:
    src: ~/Dev/projects/app/build/
    dest: /var/www/html/
    owner: www-data
    group: www-data
    mode: 0644

- name: Copy package-lock.json to server
  tags: app, build, deploy
  copy:
    src: ~/Dev/projects/app/package-lock.json
    dest: /var/www/html/package-lock.json
    owner: www-data
    group: www-data
    mode: 0644

- name: Copy package.json to server
  tags: app, build, deploy
  copy:
    src: ~/Dev/projects/app/package.json
    dest: /var/www/html/package.json
    owner: www-data
    group: www-data
    mode: 0644

- name: Create service file
  tags: app, build, deploy
  template:
    src: files/service
    dest: /etc/systemd/system/nodejs.service
  register: service_conf

- name: Reload systemd daemon
  tags: app, build, deploy, systemd
  systemd:
    daemon_reload: yes
  when: service_conf.changed

- name: Install dependencies from lockfile
  tags: app, build, deploy
  shell: npm ci
  args:
    chdir: /var/www/html/

- name: Start NodeJS service
  tags: app, build, deploy
  service:
    name: nodejs
    state: started
    enabled: yes

Ok so it’s not actually that complicated. But still, let’s break down the 8 tasks:

  1. Firstly, I need a build of the application. This is an npm script specific to SvelteKit. The shell command npm run build is being done in the directory ~/Dev/projects/app/ on my local machine via “delegate_to.”
  2. The entire build directory gets copied to the server.
  3. package-lock.json and package.json are copied to the server.
  4. A systemd service worker file is created using a template specified in files/service within this role. I also register service_conf so I can observe if the file changes.
    See my post on starting systemd services within vagrant machines for more information.
  5. If service_conf changed, reload the systemd deamon.
  6. Use npm ci to install dependencies from the lockfile.
  7. Ensure the nodejs service is running on the server.

files/service

[Unit]
Description=nodejs server

[Service]
ExecStart=/usr/bin/node /var/www/html/index.js
Restart=on-failure
# Output to syslog
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=nodejs-example
Environment=NODE_ENV=production PORT=80

[Install]
WantedBy=multi-user.target

This is the service worker template. It calls node on our application’s index.js. Notice how I’ve specified in the environment to run Node.js on port 80.

End

Now, to deploy the app is as simple as running our Node.js install followed by the app deployment. If you combine them together into one playbook…

---
- hosts: nodejs
  roles:
    - nodejs
    - app

ansible-playbook books/deploy-app.yml (or whatever you named your playbook) and you’re all set!

By Dylan Hildenbrand

Dylan Hildenbrand smiling at the camera. I have tossled, brown hair, rounded glasses, a well-trimmed and short beard. I have light complexion and am wearing a dark sweater with a white t-shirt underneath.

Author and full stack web developer experienced with #PHP, #SvelteKit, #JS, #NodeJS, #Linux, #WordPress, and #Ansible. Check out my book at sveltekitbook.dev!

Do you like these posts? Consider sponsoring me on GitHub!

2 replies on “Deploying Node.js Apps with Ansible”

This is cool, Dylan. One question though: after the deployment for the first time, if you make changes and want to deploy it again, it looks like the code here won’t trigger closing and restarting nodeJS? I do see a “Reload systemd daemon” task but it looks like that restarts the entire systemd only on a conf file change. But what if we just want to change the NodeJS app source code? Do we need to restart it manually?

Hey, thanks for the great question! If you’re just looking to restart Node.js, you can do so using the builtin service module. The very last line set’s the “nodejs” service to “started” but you could also set it to “restarted” which would restart Node.js for you. Here’s the Ansible docs:

https://docs.ansible.com/ansible/latest/collections/ansible/builtin/service_module.html

I reloaded the systemd daemon in this example as it wouldn’t pick up on the new service file otherwise. This typically only applies to the first deployment so it could likely be skipped for subsequent deploys. I just wanted to cover my bases.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.