Bozobooks.com: Fullstack k8s application blog series
Book Library React JS Front End with Firebase Authentication Deployed on k8s, with GitOps
--
Chapter 7: In this blog, we will build a ReactJS application, that will provide UI to search for books, and add them to the personal library. We will be using Firebase for authentication, and Hashicorp Vault to inject some of the secrets. We will be deploying the ReactJS application on our Kubernetes cluster, using Git Hub actions and ArgoCD.
Architectural and design decisions
The following diagram shows the design of the pages, and the components, that we will be building.
We will be building these three components, which will be rendered in the main page App.js
- Login page (
Login.js
) — This component is used for both login and signup/register a user. This integrates with Firebase to create the user credentials - Book Search page (
BookList.js
) — This component is used to search for the book, In the backend, it will callbook-info-service-svc
Service, to fetch the book information. We will be using GraphQL to query the service. - Book Library page(
Library.js
) — This component will list all the books, that are selected by the user to be added. It will callbook-library-service-svc
Service, to add and remove books from the library.
The book information interface is componentized so that it can be reused across the pages (Book.js
).
Let's now build the page
Step 1: Generate ReactJS boilerplate code
You can create a ReactJS boilerplate code, by following the instructions here. This will create a boilerplate ReactJS project, with all the required node modules, and a sample page. You can test if the page works, before proceeding.
Step 2: Add additional dependencies
We will be using some dependencies for our application, let’s install those node modules.
Material UI
We will be using Material UI to make our interface look neat. To install material UI libraries, run the following commands in the application folder.
npm install @material-ui/core
npm install @material-ui/icons
Firebase
We will need Firebase for authentication. To install firebase dependency, run the following command
npm install firebase
After installing, the package.json
should look something like the below screenshot.
Step 3: Build the Template HTML
Build the HTML pages using any WYSIWYG HTML editors, to generate the HTML and the CSS. This will help in making the site look professional. The pages can be moved to a folder with respective CSS, images, ico etc.
Identify and remove the <div> segments of the page, which needs to be rendered dynamically. In this case, We have the following components
- Book Details Tile
- Book List as a grid
- signup and login
These HTML segments need to be moved to the respective .js files so that we can start writing code around them to render those segments dynamically.
Let's now go through the complete code of the ReactJS app, I will be walking through the key segments of the code, you can download the complete code here.
Step 4: Build the application
Let's now walk through the ReactJS components, and page and add logic
App.js
App.js is the main page. The key thing to notice here is the usage of useState()
hook. We will be storing the currently logged-in user in this state. it is initialized to null
and will be filled when the login is successful.
The user value is stored in the current context using CurrentUserContext
, which uses createContext()
to create the context object, and it is passed to all the pages, by enclosing <Home/>
inside <CurrentUserContext.Provider>
tag. This way, All the pages that are rendered will have access to the current user logged in. Think of this as a global variable, implementation in ReactJS.
The following is the source code of CurrentUserContext.js
. You can read more about the context here.
Home.js
App.js
creates the user context and embeds Home.js
inside. Home.js
is the main page, which has the core logic of rendering various components.
Line 15 — The page is reading the current user context (that is passed by App.js, in the previous step), and stores into user
variable.
Line 19 — We are checking if the user is not null, if not null, we are rendering the page with Navigation tabs for BookList and Library. As seen previously, BookList will help search for the books, and Library lists all the books, that are selected by the user. HashRouter
, helps route the call to BookList
on /
and Library
on /library
(these are NavLinks
we provided in the header).
Line 33 — if the user is null, it redirects to Login
.
Login.js
Before we build the Login page, let's first configure Firebase authentication. We will need Firebase-generated authentication details before we can write the code.
Firebase Authentication
To configure firebase authentication, follow the instructions in the link
In my case, I created a bozo-book-library project in Firebase, the following is the screenshot.
In that project, I selected the Authentication provider, and selected email/password sign-in method. The following shows the screenshot of my configuration.
Once that is done, firebase generates the boilerplate code, which we can reuse. This boilerplate code has various configurations including the apiKey
, appId
, measurementId
. These configuration parameters will be stored as Vault secrets and will be injecting them at runtime. We will walk through this later in the blog.
The following is the screenshot where the boilerplate code is provided by Firebase.
Let's copy the boilerplate code into fire.js. Find below the screenshot of fire.js
As you can see the apiKey
, appId
, measurementId
configurations are removed from the source code (as they are secrets) and instead these configuration parameters are replaced with environment variables. The application expects these values to be passed as environment variables. We will be storing these values in Vault and we will be passing this to the application, as an environment variable.
Please refer to Chapter 2, for more details on how to manage secrets.
Let's now write the code for Login and Signup.
Line 10 — We are reading the user details from the context
Line 12–15 — We are defining the email
, password
, authError
and hasAccount
variables as states, so that when the value changes the userState()
hook, will update the state. We will be using hasAccount
as a boolean flag, to check if the user already has the account or not. if not, we will render the signup page
Line 18–26 — we are defining a function, that invokes firebase authentication API to authenticate the user with the passed email and password.
Line 28–33 — We are defining a function, that invokes firebase authentication API to create a new user with the passed email and password.
Line 35–37 — We sign out using firebase authentication API
Line 39–48 — This code registers a callback, to listen to the authentication state changes. We are setting the user state when the authentication state changes (login, logout). The Authentication process is implemented in an async way (as it might take time depending on the network speeds).
The function calls back when there is any change in the authentication status, that is received from the firebase authentication API. When the state changes, and if the user is not null, the user information is stored in the context (that is global).
Line 50–52 — We are using useEffect()
hook to call the authListener()
We are using useEffect()
with an empty array (second parameter) so that we can run this only on the first render, otherwise we will end up calling authListener()
on every render, which might end up with too many callbacks or nested callbacks, and freeze the page.
In the above code, we are using the <div>
that is generated by our HTML designer tool so that the component looks as per the design.
Line 59 -61— We are getting the email
and password
, and setting the state of email
and password
, this will update the state variables.
Line 65–77 — We are checking if the user has the account, if not, we are rendering the Signup
otherwise Signin
button. On Signin
, we are calling the login()
function and on signup we are calling signup()
function.
Book.js
Book.js implements the Book component, which will be used for Book Search and Book Library pages, to show the book information. The following is the design of the Book component
Let’s walk through the code
Line 37–47 — The Book component expects bookname
, bookdescription
, imagelink
, author
, bookId
as props (This needs to be passed by the host page). These props are stored in local variables and processed to be rendered.
Line 14–34 — We will be using a Snackbar component to show the messages and errors to the user. this is a pop-up box that will show the message. The methods are used to store the state or change the state of that popup window. below is the code for the Snackbar component. We have two snack bars one to show normal messages/notifications and one to show error messages.
This is the straightforward logic, of opening and closing the message in 3 seconds. Refer to Snackbar documentation for more details
Line 49 — We are once again reading the current user from the context object that is passed on by the parent page
Line 50 — We are setting the relative path for the book-library-service-svc
Let's continue with the rest of the code
Line 52–75 — The above function sends a POST request to the book-library service to add or delete the book for the logged-in user.
Line 79–93 — We are using the HTML code that is generated using the HTML design tool, and writing our code to print the book details.
Booklist.js
Now that we have the Book component ready, we should be able to show the book information on any page.
Let’s now walk through the code
Line 12–15 — We are setting the state variables, which we will be using later in the code
Line 18 — Relative path to fetch book information (book-info-service-svc)
Line 22–24 — We are using useEffect()
hook to render the page, whenever there is a change in currentPage
state or searchQuery
state, the page gets rendered.
currentPage
state changes when the user clicks the next page or the previous page links,
searchQuery
state changes, when the user sets the search query keyword, and clicks the search button or presses enter key in the search text field.
Line 26–58 — We are implementing the queryService()
function to search for the book. This is called when the user provides the search query keyword and clicks the search button or presses enter key.
We are firing a GraphQL query in this code to get the title, authors, description, and links to thumbnail images. We are calling the BookInfo Service that we built in Chapter 3.
When we receive the response, we are converting to json
in Line 49 and then stored in resultObject
, and calling setSearchResponse()
which changes the state of searchResponse
state variable, which will trigger a render.
Line 60–65 — We are implementing the onSearch()
function which is called when the user provides the search keyword and clicks the search button. we are setting the searchQuery
state and calling queryService()
the function to fetch the records
Line 67–78 — We are implementing previousPage()
and nextPage()
functions that update the currentPage
state, that will trigger a render.
Line 86–90 — We are rendering the search keyword text field and the search button. when the user clicks the button or presses enter key, it will trigger onSearch()
function, that we implemented in the above lines.
Line 93–144 — We are using the HTML code generated by the HTML design tool, to render the Book tiles in a Grid format. We are looping through the resultObject
, and passing the book details to render a Book component (Lines 122–136).
We are also providing the Next Page and Previous Page links. We have already implemented pagination on the server side to fetch 10 records at a time. Please refer to Chapter 3 for more details.
Library.js
Library.js also has similar rendering logic as BookList.js (Ideally we should have made this grid as a ReactJS component), except that we are getting the BookIds from the Book library Service and then fetching book information from the Book Information Service.
The following code shows the implementation of getLibraryBooks()
which calls Book Library Service and fetches the bookIds for the logged-in user.
The code below shows the implementation of getBooks()
which is called to fetch the book information based on bookID, which is similar to what we did in the BookList.js
The rendering of the books as a grid is very similar logic as BookList.js. Ideally, this could have been built as a ReactJS component, which accepts a list of book records, to render.
Now we have all the pages implemented, let's quickly configure our firebase secrets in Vault
Step 5: Inject Secrets from Vault
We can create the key-value pair configuration using the vault kv put
command as shown in the screenshot below. Please refer to Chapter 2 for more details on configuring Vault
Login to the vault and provide a read access policy for the new configuration by executing the following command
vault policy write bozobooks-app-policy - <<EOF
path "bozobooks/data/googleapi-config" {
capabilities = ["read"]
}
path "bozobooks/data/postgres-config" {
capabilities = ["read"]
}
path "bozobooks/data/reactjs-firebase-config" {
capabilities = ["read"]
}
EOF
The following is the deployment YAML for our Reactjs application.
As you can see between Lines 18–28 we are providing the vault annotations, to create a secrets config file and inject the secrets into it as export statements.
In Lines 35–40, we are sourcing that exported secrets config file, so that these secrets are available for the application as environment variables. This is what we are accessing in fire.js code using process.env
, as shown below
In Line 36, we are using the Docker image of the ReactJS application, lets's now build the docker image and implement GitOps to build and deploy this image to Dockerhub automatically.
Step 6: Build and Deploy the application to Kubernetes cluster with GitOps
The following is the Dockerfile to build our ReactJS application docker image.
We are using node:13.12.0-alpine
image as the base image, and creating a /app folder as the working folder, and copying package.json, running npm install, so that it downloads all the dependency modules, we are also installing react-scripts
and then copying all the application code, and finally providing the entry point command as npm start
Let's also create a Service YAML in the infrastructure repository
Let's now build the GitOps pipeline code using GitHub actions in the Application repository. This gets triggered when the developer pushes the code.
In the above code
Line 35–57 — We are checking out all the code
Line 38–49 — Sending a slack notification
Line 51–54 — We are generating a new build number
Line 56–57 — We are printing the build number for our debugging purpose
Line 59–68 — We are building the docker image
Line 70–74 — We are logging in to Dockerhub
Line 77–80 — We are tagging the local image, and pushing it to the docker hub
Line 76–93 — We are sending the status as a slack notification
Line 95–102 — We are invoking the infrastructure code to update the Build number in the Deployment YAML. You can see the code below
The following is the screenshot of the app
We still cannot test this app, unless we configure Ingress, as we are using various relative paths to call the respective services, that needs to be configured in the Ingress
In the next chapter, we will be configuring Ingress using cert-manager to implement TLS(HTTPS). Until then have a great time…see you soon
Please provide your feedback and comments.