Document Builder’s Software Architecture and Deployment

Pande Ketut Cahya Nugraha
pepeel
Published in
9 min readMar 9, 2020
A great illustration created by my teammate Firman to illustrate Document Builder’s Software Architecture. Edited to remove ambiguous interpretation of whether PostgreSQL database is in Docker or not.

According to Wikipedia, Software Architecture refers to the fundamental structures of a software system and the discipline of creating such structures and systems. Each structure comprises software elements, relations among them, and properties of both elements and relations.

From that definition, our Document Builder’s Software Architecture consists of five components. We’ll explain those components one by one.

React as Frontend

React is one of the most popular JavaScript libraries for building user interfaces. We are very lucky because our partner requires us to use react and coincidentally three of our dev team are already familiar with React. We are using create-react-app to initialize our React App, which already comes with lots of configuration to run, develop, and build React app blazingly fast.

As for the implementation detail, we use React Hooks, which allow us to use functional component instead class-based component, which in turn makes our React App (arguably) lighter, more readable, and easily understood (yay, clean code! Some might disagree though). We also use Redux, a JavaScript library to manage global application states.

React Hooks is a fairly new addition to React. It is introduced to React in 2018 and quickly became the newest hot things in Frontend Programming. Numerous factors make Hooks preferable to the traditional way of using React, and I will show some of it with examples to you.

  • Hooks is shorter and less confusing for beginners.
Left using class component and right using hooks. The class component is longer by 10 lines.

As pictured above, the class component is inherently longer than hooks due to class-related functions such as render, constructor, and lifecycle. The class component pictured above is 10 lines longer than the functional component that uses hooks. And that is while violating ESLint constraint. If we fully follow ESLint demands, it will be around 15 lines longer as we have to separate componentDidUpdate with this.setState.

Talking about componentDidUpdate and this.setState, the class component is also harder to understand for beginners due to numerous restriction and use cases of class component functions, such as this.setState that have to be placed in the correct way, and numerous lifecycles that each has a separate purpose such as componentDidUpdate that runs every time props or state changes, componentDidMount that runs on the first time component mounted on React, componentWillUnmount that runs when the component will be unmounted on React, and numerous other.

Compare this with React Hooks that conveniently wrap all of this inside useEffect hooks. Using hooks, you only need to think about what will happen when your component state and props changes. Talking about useEffect, here is another advantage of hooks.

  • Separation of concern
Left: Reloading template list and route changes can be handled separately. Right: On class component, however, both must be handled in a componentDidUpdate.

React component lifecycle are set in stone. You cannot have two separate lifecycle function such as componentDidUpdate. So, when you have two different things when a prop or a state change, both have to be handled inside componentDidUpdate, which can make understanding what happens when the props or state changes difficult.

In Hooks, however, both can be placed in different useEffect function, thus increasing readability and separating the concern into two different places.

Terminal Log when we build our React App. Webpack magic turns 87 MB code into about 300 KB code that can be loaded fast by a browser.

On deployment, the React App will be bundled by Webpack (magic!) into a small JavaScript file, complete with static assets, which then could be served either by a web server such as NGINX or served through our backend as we currently do.

ExpressJS as Backend

ExpressJS is our framework of choice for our backend services. ExpressJS is run on NodeJS, which you can think as something that runs JavaScript codes. Our backend is built as a REST API, which serves as a mediator between frontend and database. Most of our application logic, e.g. what to do when the user wants to save a new template or wants to see all templates, are located here.

Sequelize and PostgreSQL

We are using PostgreSQL as our Database Management System. Sequelize is used as ORM (Object Relational Mapping). ORM is an abstraction of the database, so we no longer think of the database as.. database, but as objects like in Object-Oriented Programming. For example, let’s say there is a SQL Table named users. Instead of thinking of it as a database table and querying all of its contents using SQL syntax, we use Sequelize to abstract it away as an object called Users which have a method called findAll that returns all of its contents.

Docker as Container Platform

Have you ever deployed an app to a server? It is very, very painful. You have to manually copy your code to the software (either by truly copy-pasting it or using git) install every software your app depends on one by one, making sure that all those software are compatible with your OS, configuring your server environment, etc. All those things can take more than a few hours. It is even worse when you have to deploy to many servers, making sure that you do all those things in the correct order and completely identical with each other.

Thankfully, Docker comes to the rescue. With docker, you can write a series of instruction, defining the dependencies and environment, and you can be sure that wherever you run that docker configuration, it will be identical.

Our docker-compose.yml file contents

See the above code? Those are our docker-compose file, which defines that our docker consists of two services, database and app server, which are connected on a private network. We define our database to use PostgreSQL 12 and configure its password and schema with a set variable, and no matter where we deploy our docker, it will use the same configuration. In case we have to deploy our app over numerous server, because of docker, all of those deployments will be completely identical.

Note: In the previous version of this article, it is stated implicitly through the first illustration that the database is placed outside of docker. However, in actuality, the database is placed inside the docker. It is placed as a separate service from backend and frontend. The Docker icon in the illustration has been moved to reflect this.

Now, while the services and configuration will be identical no matter where the service is, how about the app itself? We talked about an app server inside the docker, but how it is built with all of its dependencies? Let’s look at a file called Dockerfile.

Dockerfile contain a series of instruction telling docker how to build our app. It defines OS to be used to run the app, instruction to install dependencies needed, commands to build the app, structure of the app folder, and command to run the app. With this, our app build process will be identical no matter where the server is located.

Note that your Dockerfile may vary, according to your app’s needs. As such, the Dockerfile shown above is tailored to Document Builder’s needs. Document Builder is built using Yarn and backed by Node.js, so as seen on Stage 1 section of the Dockerfile, we using a Node.js image that comes preinstalled with Node.js, and we copied several files that defined custom yarn commands and dependency packages needed by our app to our working directory. Then, we install the dependency packages using the command yarn, copy our source code and public static files to the working directory, set the environment to tell yarn whether to build the app for local, staging, or production, and build the app using two custom yarn command.

On the Stage 2 section, we defined how to start our app in server. First, we install several system apps that are needed by Document Builder’s such as Libreoffice, Java, and fonts for PDF conversion. We also copied files that defined Document Builder dependency packages, and install it using yarn. But different from Stage 1, we only install packages that are needed to run the app in production and removing packages that are only needed for developing such as ES-Lint. Afterwards, we copy the build result from Stage 1, copy files defining environment variable and database connection, and run the app using the command node.

We talk a lot about using docker to make it easier for us to deploy an app to the server. But that doesn’t answer how we actually deploy an app to the server. So how do we actually deploy our Document Builder app?

Complying to the standard practice of the industry today (and because we are required to use it on PPL 2020), we use a CI/CD pipeline to define a series of instruction that is automatically run to deploy our app. Let’s examine it step-by-step.

Where is our CI/CD pipeline located?

Because we use GitLab hosted by our university to host our code, we can use GitLab CI/CD feature to run a CI/CD pipeline that is defined in .gitlab-ci.yml file. To run those pipeline, GitLab uses special processes called runners. Our university provides some shared runners for us to use, but our teammate Firman set up a private runner that is exclusively used by us.

Thanks, Firman! No more need to wait in line with other teams.

Stages of the CI/CD Pipeline

Our CI/CD Pipeline Stages

Our CI/CD Pipeline is separated into four stages, that is:

  1. Analysis, which runs linter to ensure our code syntax is correct and clean and runs testing to ensure our code passes all tests. It also fetches the coverage percentage to be displayed on README and run further code analysis on SonarCube which is provided by our university. While further stages are only run when we push code to branch staging, this stage is always run whenever we push code to anywhere.
Analysis Stage of our CI/CD Pipeline.

2. After passing the Analysis stage, we enter the Image stage. On this stage, we build our app image that is defined using Dockerfile. To do this, we rely on a docker building service called Kaniko. We use this service because using docker require root access, so if we build the image directly in our server, this risk exposing our server and making it insecure. Thus, we use Kaniko which will build our image remotely and send the result to dockerhub. The image in dockerhub then can be downloaded by our server to be used. Note that there are two different image stage, each for staging and production servers.

Image Stage. We use Kaniko to build the docker image. The stage also uses different ENV for staging and production.

3. Next is the Predeploy Stage. On this stage, our runner will connect to our server via SSH using SSH Private Key. Once inside the server, it will run migration for our database, so the database service schema will be in sync with the latest definition. As with Image stage, there is a separate stage for staging and master branch, as both are deployed to a different server.

The Predeploy Stage.

4. The last stage is the Deploy Stage. Here, the pipeline will download the image previously built on Image stage, and run it on docker using the docker-compose command. The docker-compose command will then set up the services according to docker-compose file, and then the app contained within the image will be run.

The Deploy Stage. Note that both also depend on sshrun, the same as Predeploy Stage. sshrun define the required command to connect to our server via SSH.

That is all about our Document Builder’s Software Architecture and Deployment, and I hope it can clearly describe Document Builder’s Software Architecture and Deployment well enough! Don’t hesitate to ask questions on the comments!

--

--