Modern applications on OpenShift, part 2: chained builds

Hi all! This is the second post in our series where we show you how to deploy modern web applications on Red Hat OpenShift.

Modern applications on OpenShift, part 2: chained builds

In the previous post, we briefly touched on the capabilities of the new S2I (source-to-image) builder image, which is designed to build and deploy modern web applications on the OpenShift platform. Then we were interested in the topic of rapid application deployment, and today we will look at how to use an S2I image as a β€œclean” builder image and combine it with related OpenShift builds.

Clean builder image

As we mentioned in the first part, most modern web applications have a so-called build stage, which typically performs operations such as transpiling code, concatenating multiple files, and minifying. The files obtained as a result of these operations - and this is static HTML, JavaScript and CSS - are added to the output folder. The location of this folder usually depends on which build tools are being used, and for React it would be the ./build folder (we'll come back to this in more detail below).

Source-to-Image (S2I)

In this post, we do not touch on the topic β€œwhat is S2I and how to use it” at all (you can read more about this here), but it's important to be clear about the two steps of this process in order to understand what the Web App Builder image does.

Assembly phase

The assembly step is essentially very similar to what happens when you run docker build and end up with a new Docker image. Accordingly, this stage occurs when starting the assembly on the OpenShift platform.

In the case of the Web App Builder image, installing your application's dependencies and running the build is the responsibility of assemble script. By default, the builder image uses the npm run build construct, but it can be overridden via the NPM_BUILD environment variable.

As we said earlier, the location of the finished, already built application depends on which tools you use. For example, in the case of React, this will be the ./build folder, and for Angular applications, it will be the project_name/dist folder. And, as already shown in the last post, the location of the output directory, which is set to build by default, can be overridden through the OUTPUT_DIR environment variable. Well, since the location of the output folder differs from framework to framework, you simply copy the generated output to the standard folder in the image, namely /opt/apt-root/output. This is important for understanding the remainder of this article, but for now, let's take a quick look at the next phase, the run phase.

Run phase

This step occurs when a call to docker run is made on the new image created in the assembly step. It also happens when deploying to the OpenShift platform. Default run script uses serve module to serve static content in the above standard output directory.

This method is good for quickly deploying applications, but it's generally not recommended to serve static content this way. Well, since we really only serve static content, we don’t need Node.js installed inside our image - a web server is enough.

In other words, when building, we need one thing, when executing, we need another. This is where chained builds come in handy.

Chained builds

Here is what they write about chained builds in the OpenShift documentation:

"Two assemblies can be linked together, with one generating a compiled entity and the other hosting that entity in a separate image that is used to run that entity."

In other words, we can use the Web App Builder image to run our build and then use the NGINX web server image to serve our content.

Thus, we can use the Web App Builder image as a "pure" builder and at the same time have a small runtime image.

Now let's take a look at a specific example.

For training we will use simple React app, created with the create-react-app command line tool.

Putting it all together will help us OpenShift template file.

Let's analyze this file in more detail, and start with the parameters section.

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 web apps builder
    displayName: Output Directory
    value: build
    required: false

Everything is pretty clear here, but you should pay attention to the OUTPUT_DIR parameter. For the React app in our example, there is nothing to worry about, since React uses the default output folder, but in the case of Angular or something else, this setting will need to be changed as needed.

Now let's take a look at the ImageStreams section.

- apiVersion: v1
  kind: ImageStream
  metadata:
    name: react-web-app-builder  // 1 
  spec: {}
- apiVersion: v1
  kind: ImageStream
  metadata:
    name: react-web-app-runtime  // 2 
  spec: {}
- apiVersion: v1
  kind: ImageStream
  metadata:
    name: web-app-builder-runtime // 3
  spec:
    tags:
    - name: latest
      from:
        kind: DockerImage
        name: nodeshift/ubi8-s2i-web-app:10.x
- apiVersion: v1
  kind: ImageStream
  metadata:
    name: nginx-image-runtime // 4
  spec:
    tags:
    - name: latest
      from:
        kind: DockerImage
        name: 'centos/nginx-112-centos7:latest'

Look at the third and fourth images. They are both defined as Docker images, and you can clearly see where they come from.

The third image is web-app-builder and is taken from nodeshift/ubi8-s2i-web-app tagged 10.x on docker hub.

The fourth is an NGINX image (version 1.12) with the latest tag on docker hub.

Now let's look at the first two images. They are both empty at the start and are only created during the build phase. The first image, react-web-app-builder, will be the result of an assembly step that will combine the web-app-builder-runtime image and our source code. That's why we put "-builder" in the name of this image.

The second image - react-web-app-runtime - will be the result of combining nginx-image-runtime and some files from the react-web-app-builder image. This image will also be used during deployment and will only contain the web server and the static HTML, JavaScript, CSS of our application.

Confusing? Now let's take a look at the build configurations and it will become a little clearer.

Our template has two build configurations. Here's the first one, and it's pretty standard:

  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:
        env:
          - name: OUTPUT_DIR // 3 
            value: ${OUTPUT_DIR}
        from:
          kind: ImageStreamTag
          name: web-app-builder-runtime:latest // 4
        incremental: true // 5
      type: Source
    triggers: // 6
    - github:
        secret: ${GITHUB_WEBHOOK_SECRET}
      type: GitHub
    - type: ConfigChange
    - imageChange: {}
      type: ImageChange

As you can see, the line labeled 1 says that the result of this build will be placed in the same react-web-app-builder image that we saw a little earlier in the ImageStreams section.

The line labeled 2 tells where to get the code from. In our case, this is a git repository, and the location, ref, and context folder are defined by the parameters we have already seen above.

The line labeled 3 is what we have already seen in the parameters section. It adds the OUTPUT_DIR environment variable, which in our example is equal to build.
The line labeled 4 says to use the web-app-builder-runtime image, which we already saw in the ImageStream section.

The line labeled 5 says we want to use an incremental build if the S2I image supports it, and the Web App Builder image does. On first run, after the assembly step is complete, the image will save the node_modules folder to an archive file. Then, on subsequent runs, the image will simply unzip this folder to reduce the build time.

And finally, the line labeled 6 is just a few triggers to make the build run automatically, without manual intervention, when something changes.

In general, this is a fairly standard build configuration.

Now let's take a look at the second build configuration. It is very similar to the first, but there is one important difference.

apiVersion: v1
  kind: BuildConfig
  metadata:
    name: react-web-app-runtime
  spec:
    output:
      to:
        kind: ImageStreamTag
        name: react-web-app-runtime:latest // 1
    source: // 2
      type: Image
      images:                              
        - from:
            kind: ImageStreamTag
            name: react-web-app-builder:latest // 3
          paths:
            - sourcePath: /opt/app-root/output/.  // 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

So, the second build configuration is react-web-app-runtime, and it starts out pretty standard.

The line labeled 1 is nothing new - it just says that the build result is put into the react-web-app-runtime image.

The line labeled 2, as in the previous configuration, indicates where to get the source code from. But note that here we say that it is taken from the image. Moreover, from the image that we just created - from the react-web-app-builder (indicated in the line labeled 3). The files we want to use are inside the image and their location there is given on line labeled 4, in our case /opt/app-root/output/. If you remember, this is where the files generated from the results of building our application are added.

The destination folder given at the time labeled 5 is just the current directory (remember, this is all running inside some magical thing called OpenShift, not on your local computer).

The strategy section, the line labeled 6, is also similar to the first build configuration. Only this time we are going to use nginx-image-runtime which we have already seen in the ImageStream section.

Finally, the line labeled 7 is the trigger section that fires this build every time the react-web-app-builder image changes.

Otherwise, this template contains a fairly standard deployment configuration, as well as things that relate to services and routes, but we will not go into that. Note that the image that will be deployed is the react-web-app-runtime image.

Application Deployment

So, after we've taken a look at the template, let's see how to use it to deploy an application.

We can use the OpenShift client tool called 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 in the screenshot above is a deliberately engineered way to find the ./openshiftio/application.yaml template.

The second command simply creates a new application based on this template.

After these commands work, we will see that we have two assemblies:

Modern applications on OpenShift, part 2: chained builds

And returning to the Overview screen, we will see the launched pod:

Modern applications on OpenShift, part 2: chained builds

Clicking on the link will take us to our app, which is the default React App page:

Modern applications on OpenShift, part 2: chained builds

1 add-on

For Angular lovers, we also have application example.

The template is the same here, except for the OUTPUT_DIR variable.

2 add-on

In this article, we used NGINX as a web server, but it is quite easy to replace it with Apache, just change the template in the file nginx image on Apache image.

Conclusion

In the first part of this series, we showed you how to quickly deploy modern web applications on the OpenShift platform. Today we looked at what a Web App image does and how it can be combined with a pure NGINX web server using chained builds to build applications more suitable for production environments. In the next and final article of this series, we'll show you how to run a development server for your application on OpenShift and keep local and remote files in sync.

Contents of this article series

  • Part 1: how to deploy modern web applications in just a few steps;
  • Part 2: how to use a new S2I image along with an existing HTTP server image, such as NGINX, using linked OpenShift assemblies, to organize a production deployment;
  • Part 3: how to run a development server for your application on the OpenShift platform and synchronize it with the local file system.

Additional resources

Source: habr.com

Add a comment