In the previous article, we took a quick look at a new source-to-image (S2I) builder image designed for building and deploying modern web applications on Red Hat OpenShift. While the last article was focused on getting your application deployed quickly, this article will look at how to use the S2I image as a "pure" builder image and combine it with an OpenShift chained build.
Read the series:
-
Part 1: How to deploy modern web apps using the fewest steps
-
Part 2: Combine a Node.js Builder image with a current HTTP server image
-
Part 3: Run your application's development server on OpenShift while syncing with your local file system
Pure builder image
As mentioned in the previous article, most modern web applications now have a build step. Common workflows done in the build step are things like transpiling your code, concatenating multiple files, and minifying. Once these workflows are done, the resulting files, which are static HTML, JavaScript, and CSS, are put into an output folder. The location of the folder usually depends on the build tools you are using, but for something like React, the location is ./build
(more on this location in a minute).
Source-to-Image (S2I)
We won't go into the "what and how" of S2I, but we should understand two of the phases that happen in order to better understand what the Node.js Builder image is doing.
Assemble phase
The assemble phase is very similar to what happens when running docker build
. The result of this phase will be a new Docker image. This phase also happens when a build is run on OpenShift.
For the Web App Builder image, the assemble script is responsible for installing your application's dependencies and running your build. By default, the builder image will use npm run build
.
As I said before, the location of your "built" app depends on the build tools you are using. For example, React uses ./build
, but an Angular app uses project_name/dist
. And, as you saw in Part 1, this output directory, which defaults to build
, can be overridden using the OUTPUT_DIR
environment variable.
Run phase
This phase is run when docker run
is called on the newly created image from the assemble phase. This is also what is run during an OpenShift deployment. By default, the run script will run your package.json
’s “start” script.
While this works for getting your app deployed quickly, it is not the recommended way of serving static content. Because we are serving only static content, we don't really need Node.js installed in our image; we just need a web server.
This situation—where our building needs are different from our runtime needs—is where chained builds can help.
Chained builds
To quote the official OpenShift documentation on chained builds:
Two builds can be chained together: one that produces the compiled artifact, and a second build that places that artifact in a separate image that runs the artifact.
What this means is that we can use the Web App Builder image to run our build, and then we can use a web server image, like NGINX, to serve our content.
This allows us to use the Web App Builder image as a "pure" builder and also keep our runtime image small.
Let's take a look at an example to see how this all comes together.
This example app, is a basic React application created using the create-react-app
CLI tool.
I've added an OpenShift template file to piece everything together.
Let's take a look at some of the more important parts of this file.
parameters:
- name: SOURCE_REPOSITORY_URL
description: The source URL for the application
displayName: Source URL
required: true
- name: SOURCE_REPOSITORY_REF
description: The branch name for the application
displayName: Source Branch
value: master
required: true
- name: SOURCE_REPOSITORY_DIR
description: The location within the source repo of the application
displayName: Source Directory
value: .
required: true
- name: OUTPUT_DIR
description: The location of the compiled static files from your Node.js builder
displayName: Output Directory
value: build
required: false
The parameter section should be pretty self-explanatory, but I want to call out the OUTPUT_DIR
parameter. For our React example, we don't need to worry about it, since the default value is what React uses, but if you are using Angular or something else, you could change it.
Now let's take a look at the image streams.
- apiVersion: v1
kind: ImageStream
metadata:
name: react-web-app-builder // 1
spec: {}
- apiVersion: v1
kind: ImageStream
metadata:
name: static-web-app-running-on-nginx // 2
spec: {}
- apiVersion: v1
kind: ImageStream
metadata:
name: node-ubi-s2i-image // 3
spec:
tags:
- name: latest
from:
kind: DockerImage
name: registry.access.redhat.com/ubi8/nodejs-14:latest
- apiVersion: v1
kind: ImageStream
metadata:
name: nginx-image-runtime // 4
spec:
tags:
- name: latest
from:
kind: DockerImage
name: 'centos/nginx-112-centos7:latest'
First, let's take a look at the third and fourth images. We can see that both are defined as Docker images, and we can see where they come from.
The third is the Node S2I image, registry.access.redhat.com/ubi8/nodejs-14
, which is using the latest tag.
The fourth is an NGINX image (version 1.12) using the latest tag from the Docker hub.
Now, let's take a look at those first two images. Both images are empty to start. These images will be created during the build phase, but for completeness, let me explain what will go into each one.
The first image, react-web-app-builder
, will be the result of the "assemble" phase of the node-ubi-s2i-image image once it is combined with our source code. That is why I've named it "-builder
."
The second image, static-web-app-running-on-nginx, will be the result of combining the nginx-image-runtime
with the some of the files from the react-web-app-builder
image. This image will also be the image that is "deployed" and will contain only the web server and the static HTML, JavaScript, and CSS for the application.
This might sound a little confusing now, but once we look at the build configurations, things should be a little more clear.
In this template, there are two build configurations. Let's take a look at them one at a time.
apiVersion: v1
kind: BuildConfig
metadata:
name: react-web-app-builder
spec:
output:
to:
kind: ImageStreamTag
name: react-web-app-builder:latest // 1
source: // 2
git:
uri: ${SOURCE_REPOSITORY_URL}
ref: ${SOURCE_REPOSITORY_REF}
contextDir: ${SOURCE_REPOSITORY_DIR}
type: Git
strategy:
sourceStrategy:
from:
kind: ImageStreamTag
name: node-ubi-s2i-image:latest // 3
type: Source
triggers: // 4
- github:
secret: ${GITHUB_WEBHOOK_SECRET}
type: GitHub
- type: ConfigChange
- imageChange: {}
type: ImageChange
The first one, react-web-app-builder
, is pretty standard. We see that line 1 tells us the result of this build will be put into the react-web-app-builder
image, which we saw when we took a look at the image stream list above.
Next, line 2 is just telling us where the code is coming from. In this case, it is a Git repository, and the location, ref
, and context directory are defined by the parameters we saw earlier.
Line 3 is just telling us to use the node-ubi-s2i-image
image that we saw in the ImageStream
section
The last thing to call out, line 4, is just a few triggers that are set up, so when something changes, this build can be kicked off without manual interaction.
As I said before, this is a pretty standard build configuration. Now let's take a look at the second build configuration. Most of it is very similar to the first, but there is one important difference:
apiVersion: v1
kind: BuildConfig
metadata:
name: static-web-app-running-on-nginx
spec:
output:
to:
kind: ImageStreamTag
name: static-web-app-running-on-nginx:latest // 1
source: // 2
type: Image
images:
- from:
kind: ImageStreamTag
name: react-web-app-builder:latest // 3
paths:
- sourcePath: /opt/app-root/src/${OUTPUT_DIR}/. // 4
destinationDir: . // 5
strategy: // 6
sourceStrategy:
from:
kind: ImageStreamTag
name: nginx-image-runtime:latest
incremental: true
type: Source
triggers:
- github:
secret: ${GITHUB_WEBHOOK_SECRET}
type: GitHub
- type: ConfigChange
- type: ImageChange
imageChange: {}
- type: ImageChange
imageChange:
from:
kind: ImageStreamTag
name: react-web-app-builder:latest // 7
This second build configuration, static-web-app-running-on-nginx, starts off in a fairly standard way.
Line 1 isn't anything new. It is telling us that the result of this build will be put into the static-web-app-running-on-nginx image.
As with the first build configuration, we have a source section, line 2, but this time we say our source is coming from an image. The image that it is coming from is the one we just created, react-web-app-builder
(specified in line 3). The files we want to use are located inside the image and that location is specified in line 4: /opt/app-root/src/${OUTPUT_DIR}/
. If you remember, this is where our generated files from our application's build step ended up.
The destination directory, specified in line 5, is just the current directory (this is all happening inside some magic OpenShift thing, not on your local computer).
The strategy section, line 6, is also similar to the first build configuration. This time, we are going to use the nginx-image-runtime
that we looked at in the ImageStream
section.
The final thing to point out is the trigger section, line 7, which will trigger this build anytime the react-web-app-builder
image changes.
The rest of the template is fairly standard deployment configuration, service, and route stuff, which we don't need to go into. Note that the image that will be deployed will be the react-web-app-runtime
image.
Deploying the application
Now that we've taken a look at the template, let's see how we can easily deploy this application.
We can use the OpenShift client tool, oc
, to deploy our template:
$ find . | grep openshiftio | grep application | xargs -n 1 oc apply -f
$ oc new-app --template react-web-app -p SOURCE_REPOSITORY_URL=https://github.com/lholmquist/react-web-app
The first command is just an overly engineered way of finding the ./openshiftio/application.yaml
template. The second creates a new application based on that template. Once those commands are run, we can see that there are two builds:
Back on the Overview screen, we should see the running pod:
Clicking the link should navigate to our application, which is the default React application page:
Extra credit: Make it an Angular application
For developers who are into using Angular, here is an example of that. The template is mostly the same, except for that OUTPUT_DIR
variable.
Extra, extra credit: Swap NGINX with Apache web server
This article showed how to use the NGINX image as our web server, but it's fairly easy to swap that out if you wanted to use an Apache server. It can actually be done in one or maybe two (for completeness) steps.
All you need to do is in the template file, swap out the NGINX image for the Apache image.
Summary
While the first article in this series showed how to quickly get a modern web application on OpenShift, this article went deeper into what the Node.js Builder image is doing and how to combine it, using a chained build, with a pure web server such as NGINX for a more production-ready build.
In the next and final article, we will take a look at how to run our web application's development server on OpenShift, while keeping our local and remote files in sync.
Additional resources
- Deploying to OpenShift: a guide for impatient developers: Get the free ebook.
- Learn more about OpenShift and Kubernetes.