Modern OpenShift apps, part 2: chained builds

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







In the previous post, we slightly 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 assemblies.



Clean builder image



As we mentioned in the first part, most modern web applications have a so-called build stage, which usually performs operations such as code transpilation, concatenation of multiple files, and minification. The resulting files - static HTML, JavaScript and CSS - are added to the output folder. The location of this folder usually depends on which build tools are used, and for React this will be the ./build folder (we'll get back to this in more detail below).



Source-to-Image (S2I)



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



Assemble phase



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



In the case of a Web App Builder image, the assemble script is responsible for installing your application's dependencies and running the build . 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, the project_name / dist folder. And, as shown in the last post, the location of the output directory, which is set to build by default, can be overridden via 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 rest of this article, but for now let's take a quick look at the next stage - the run (run phase).



Run phase



This stage occurs when docker run is called on a new image created during the assembly stage. It also happens when deploying on the OpenShift platform. By default, the run script uses the serve module to serve static content located in the above standard output directory.



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



In other words, we need one thing during assembly, and another during execution. This is where chained builds come in handy.



Chained builds



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



"Two assemblies can be linked to each other, 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 web server image, NGINX, to serve our content.



Thus, we can use the Web App Builder image as a pure builder and still have a small runtime image.



Now let's look at this with a specific example.



For this tutorial, we'll use a simple React application built with the create-react-app command line tool.



The OpenShift template file will help us put everything together .



Let's take a closer look at this file 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 application from our example, there is nothing to worry about, since React uses the default value as the output folder, but in the case of Angular or something else, this parameter 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'


Take a 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 it comes from nodeshift / ubi8-s2i-web-app with 10.x tag on Docker hub .



The fourth is an NGINX image (version 1.12) tagged latest on Docker hub .



Now let's take a look at the first two images. They are both empty at start and are only created at the build phase. The first image, react-web-app-builder, will be the result of an assembly step that will merge the web-app-builder-runtime image and our source code. That is 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.



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



There are two build configurations in our template. 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 earlier in the ImageStreams section.



The line labeled 2 tells you 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 already seen in the parameters section. It adds the OUTPUT_DIR environment variable, which is build in our example.

The line labeled 4 says to use the web-app-builder-runtime image we already saw in the ImageStream section.



The line labeled 5 says that we want to use an incremental build if the S2I image supports it and the Web App Builder image does. On first launch, after completing the assembly phase, the image will save the node_modules folder to an archive file. Then, on subsequent runs, the image will simply unzip that folder to shorten the build time.



Finally, the line labeled 6 is just a few triggers so that the build starts automatically, without manual intervention, when something changes.



All in all, this is a pretty 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 being 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 notice that here we say that it is taken from the image. Moreover, from the image that we just created - from react-web-app-builder (indicated in the line labeled 3). The files we want to use are located inside the image and their location is specified there on the line labeled 4, in our case it is / opt / app-root / output /. If you remember, this is where the files generated from the results of building our application are placed.



The destination folder, specified in the line labeled 5, is just the current directory (remember, this is all spinning inside some magical thing called OpenShift, not on your local computer).



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



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



The rest of 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.



Deploy the application



So, after taking a look at the template, let's see how to use it to deploy the application.



We can use an 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 engineering way to find the template. / Openshiftio / application.yaml.



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



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







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







Click on the link and we'll be taken to our app, which is the default React App page:







Appendix 1



For Angular lovers, we also have a sample app .



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



Appendix 2



In this article, we used NGINX as a web server, but it is quite easy to replace it with Apache, just change the NGINX image in the template file to an 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 makes a Web App image and how it can be combined with a pure web server like NGINX using chained builds to create a more production-ready app build. In the next, final article in 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










All Articles